Forráskód Böngészése

feat: 新增多类功能并修复多处bug

1. 新增设备默认图标:SOPHA、STEM系列、SOMIA、SMOID系列等多款设备svg图标
2. 新增设备详情页面:weatherStation2和SoilMoisturelist及其子页面、图表组件
3. 新增虫害AI智能分析功能与markdown渲染组件
4. 新增权限控制:平鲁农业农村局用户无法操作灌溉设备
5. 修复pest_list空值问题、图表y轴刻度重复问题
6. 优化设备列表跳转路径与归档页面显示逻辑
7. 新增设备控制页面,支持重启、升级、设置上传间隔
8. 调整虫害详情页布局与滚动逻辑
allen 13 órája
szülő
commit
b938c8a916
41 módosított fájl, 7327 hozzáadás és 23 törlés
  1. 76 0
      pages.json
  2. 22 1
      pages/cb/shuifeizsFirst/shuifeizs.vue
  3. 518 0
      pages/cbd/components/aiChat.vue
  4. 122 0
      pages/cbd/components/markdownRenderer.js
  5. 1 1
      pages/cbd/components/pestArchive.vue
  6. 21 0
      pages/cbd/components/pestEchart.vue
  7. 248 0
      pages/cbd/components/sse.js
  8. 45 5
      pages/cbd/detail.vue
  9. 121 0
      pages/deviceDetails/SoilMoisturelist/charts.vue
  10. 148 0
      pages/deviceDetails/SoilMoisturelist/devControl.vue
  11. 350 0
      pages/deviceDetails/SoilMoisturelist/devRepair.vue
  12. 899 0
      pages/deviceDetails/SoilMoisturelist/eleDetail.vue
  13. 1456 0
      pages/deviceDetails/SoilMoisturelist/index.vue
  14. 345 0
      pages/deviceDetails/SoilMoisturelist/simDetail.vue
  15. 121 0
      pages/deviceDetails/weatherStation2/charts.vue
  16. 181 0
      pages/deviceDetails/weatherStation2/devControl.vue
  17. 350 0
      pages/deviceDetails/weatherStation2/devRepair.vue
  18. 899 0
      pages/deviceDetails/weatherStation2/eleDetail.vue
  19. 996 0
      pages/deviceDetails/weatherStation2/index.vue
  20. 345 0
      pages/deviceDetails/weatherStation2/simDetail.vue
  21. 13 2
      pages/equipList2/index.vue
  22. 1 1
      pages/sy/devicePhoto.vue
  23. 1 1
      static/images/device/HUMIA-default.svg
  24. 4 4
      static/images/device/ILLUA-default.svg
  25. 3 3
      static/images/device/PRECA-default.svg
  26. 2 2
      static/images/device/PRESA-default.svg
  27. 3 0
      static/images/device/SMOID-default.svg
  28. 3 0
      static/images/device/SMOIF-default.svg
  29. 3 0
      static/images/device/SMOIG-default.svg
  30. 3 0
      static/images/device/SMOIH-default.svg
  31. 3 0
      static/images/device/SOMIA-default.svg
  32. 3 0
      static/images/device/SOMIB-default.svg
  33. 3 0
      static/images/device/SOMIC-default.svg
  34. 3 0
      static/images/device/SOPHA-default.svg
  35. 3 0
      static/images/device/STEMD-default.svg
  36. 3 0
      static/images/device/STEMF-default.svg
  37. 3 0
      static/images/device/STEMG-default.svg
  38. 3 0
      static/images/device/STEMH-default.svg
  39. 1 1
      static/images/device/TEMPA-default.svg
  40. 1 1
      static/images/device/WDIRA-default.svg
  41. 1 1
      static/images/device/WSPDA-default.svg

+ 76 - 0
pages.json

@@ -1316,6 +1316,82 @@
         "navigationStyle": "custom"
       }
     },
+
+    
+    {
+      "path": "pages/deviceDetails/weatherStation2/index",
+      "style": {
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/deviceDetails/weatherStation2/devControl",
+      "style": {
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/deviceDetails/weatherStation2/simDetail",
+      "style": {
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/deviceDetails/weatherStation2/devRepair",
+      "style": {
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/deviceDetails/weatherStation2/eleDetail",
+      "style": {
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+
+
+    
+    {
+      "path": "pages/deviceDetails/SoilMoisturelist/index",
+      "style": {
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/deviceDetails/SoilMoisturelist/devControl",
+      "style": {
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/deviceDetails/SoilMoisturelist/simDetail",
+      "style": {
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/deviceDetails/SoilMoisturelist/devRepair",
+      "style": {
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/deviceDetails/SoilMoisturelist/eleDetail",
+      "style": {
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+
     {
       "path": "pages/warning/index",
       "style": {

+ 22 - 1
pages/cb/shuifeizsFirst/shuifeizs.vue

@@ -10,7 +10,7 @@
         </view>
       </block>
     </custom-card>
-    <view class="facilitystate-top">
+    <view class="facilitystate-top" v-if="username !== '平鲁农业农村局'">
       <view
         class="facilitystate-top__item"
         v-for="(item, index) in deviceList"
@@ -159,6 +159,7 @@ export default {
       clearIrr: {},
       selectAreaList: [],
       runTitle: '--',
+      username: '',
       timer:'--'
     };
   },
@@ -191,6 +192,12 @@ export default {
     if (devBid) {
       this.init();
     }
+    uni.getStorage({
+      key: 'user_name',
+      success: (res) => {
+        this.username = res.data
+      }
+    })
   },
   onUnload() {
     this.ws.close();
@@ -268,6 +275,13 @@ export default {
       });
     },
     changeActive(active) {
+      if(this.username == '平鲁农业农村局'){
+        uni.showToast({
+          title: '暂无权限',
+          icon: 'none',
+        });
+        return
+      }
       this.runStatus = active;
       const params = {
         IrrStatus:active
@@ -275,6 +289,13 @@ export default {
       this.devctlContorl(params);
     },
     changeMode(mode) {
+      if(this.username == '平鲁农业农村局'){
+        uni.showToast({
+          title: '暂无权限',
+          icon: 'none',
+        });
+        return
+      }
       if(this.runStatus == '1' || this.runStatus == '2'){
         uni.showToast({
           title: '请先停止运行',

+ 518 - 0
pages/cbd/components/aiChat.vue

@@ -0,0 +1,518 @@
+<template>
+  <view class="deepseek-chat-container">
+    <scroll-view class="chat-messages" scroll-y :scroll-into-view="scrollIntoView" v-show="pestMessage">
+      <view class="message-content">
+        <view class="message-input" style="padding:0 20rpx">{{ pestMessage }}</view>
+        <view v-if="isloading" class="loading-hint">AI 正在分析中…</view>
+        <view v-if="errorMsg" class="loading-hint error-hint">{{ errorMsg }}</view>
+        <rich-text class="message-text markdown-output" :nodes="outputHtml"></rich-text>
+        <view class="message-input footer">内容由大模型生成,仅供参考,相关风险需自行承担。</view>
+      </view>
+      <view id="chatBottomAnchor"></view>
+    </scroll-view>
+    <view v-if="!pestMessage" style="height:100%">
+
+    </view>
+    <!-- App 端 renderjs SSE 桥接:仅 App 渲染(H5/小程序走 sse.js,不触发 renderjs)。
+         用 v-if="isApp" 运行时门控,避免 H5 上 change:prop 随组件重渲染被反复触发。 -->
+    <view v-if="isApp" :sseSignal="sseSignal" :change:sseSignal="sse.onSignalChange" style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0"></view>
+  </view>
+</template>
+
+<script>
+import { createSSEConnection } from './sse.js';
+import { renderMarkdown } from './markdownRenderer.js';
+
+export default {
+  name: 'DeepSeekChat',
+  props: {
+    pestMessage: {
+      type: String,
+      default: ''
+    },
+  },
+  data() {
+    return {
+      isApp: typeof plus !== 'undefined', // 运行时判断是否 App:H5/小程序走 sse.js,不启用 renderjs
+      text: '',
+      sseConnection: null,
+      sseUrl: '',          // 当前请求地址(供 renderjs 失败时降级复用)
+      sseSignal: null,     // 与 renderjs 通信的信号(App 端用)
+      result: '',
+      thinking: false,
+      activeNames: ['1'],
+      thinkHtml: '',
+      isloading: true,
+      errorMsg: '',    // 连接失败/超时时的提示文案
+      mdParser: null,
+      mdRawText: '',   // 累积的原始 markdown 文本
+      outputHtml: '',  // renderMarkdown 渲染后的 HTML,交给 rich-text 展示
+      scrollIntoView: '', // scroll-view 滚动到底部锚点用
+      sseTimer: null,  // 连接“一条数据都没收到”的超时兜底定时器
+    };
+  },
+  watch: {
+    pestMessage(newVal) {
+      if (newVal) {
+        this.connectSSE(newVal);
+      } else {
+        this.isloading = false;
+      }
+    }
+  },
+  methods: {
+    resetStreamState() {
+      this.mdRawText = '';
+      this.outputHtml = '';
+      this.text = '';
+      this.errorMsg = '';
+      this._thinkContent = '';
+      this._isCollecting = false;
+      this._receivedAny = false;
+    },
+    connectSSE(message) {
+      this.clearSseTimer();
+      // 关闭上一次 sse.js 连接;App 的 renderjs 旧 XHR 由 start() 内部 stop() 清理,
+      // 这里不再调 disconnectSSE(),避免在 App 上多发一个 stop 信号导致 onSignalChange 多触发一次
+      if (this.sseConnection) {
+        this.sseConnection.close();
+        this.sseConnection = null;
+      }
+      this.resetStreamState();
+      this.isloading = true; // 进入“分析中”
+
+      // message 含中文/逗号/冒号,必须编码后拼到 URL
+      const sseUrl = `https://ai.nyzhwlw.com/analysis?message=${encodeURIComponent(message)}`;
+      this.sseUrl = sseUrl;
+      this._fallbackUsed = false;
+
+      // 超时兜底:30s 内一条数据都没收到(含 <think>),判定为连接异常
+      this.startTimeout();
+
+      // App 走 renderjs(WebView 原生 XHR,逐字流式);H5/小程序走 sse.js
+      if (this.isApp) {
+        this.startViaRenderjs(sseUrl);
+      } else {
+        this.startViaSseJs(sseUrl);
+      }
+    },
+    startTimeout() {
+      this.sseTimer = setTimeout(() => {
+        if (!this._receivedAny) {
+          this.errorMsg = 'AI 分析超时,请稍后重试';
+          this.isloading = false;
+          this.disconnectSSE();
+        }
+      }, 30000);
+    },
+    // H5/小程序(及 App 降级):用 sse.js 连接
+    startViaSseJs(url) {
+      this.sseConnection = createSSEConnection(url);
+      this.sseConnection.onMessage((data) => this.handleChunk(data));
+      this.sseConnection.onError((err) => this.onStreamError(err));
+      this.sseConnection.onClose(() => this.onStreamClose());
+    },
+    // App:通过 renderjs 桥接,WebView 用原生 XHR 接收流式数据
+    startViaRenderjs(url) {
+      this.sseSignal = { action: 'start', url };
+    },
+    stopRenderjs() {
+      this.sseSignal = { action: 'stop' };
+    },
+    // 三端共用的数据处理:think 块过滤 + markdown 累积渲染
+    handleChunk(data) {
+      this._receivedAny = true;
+      this.isloading = false;
+      if (data === 'yfkj@2025' || data === '[DONE]') {
+        return;
+      }
+      // 处理 <think>...</think> 思考块:模板不展示思考内容,只保留 </think> 之后的正文。
+      // (原实现里 think 用到的 mdParser 未初始化会抛错,导致后续正文也不渲染,这里整体丢弃思考块)
+      if (this._isCollecting || data.indexOf('<think>') > -1) {
+        if (data.indexOf('<think>') > -1) {
+          this._isCollecting = true;
+          this.thinking = true;
+        }
+        this._thinkContent += data;
+        const endIdx = this._thinkContent.indexOf('</think>');
+        if (endIdx === -1) {
+          return; // 仍在思考块内,等待结束标记
+        }
+        data = this._thinkContent.slice(endIdx + '</think>'.length);
+        this._thinkContent = '';
+        this._isCollecting = false;
+        this.thinking = false;
+        if (!data) {
+          return;
+        }
+      }
+      // 累积正文并渲染为 markdown,用 rich-text 展示
+      this.mdRawText += data;
+      this.outputHtml = renderMarkdown(this.mdRawText);
+      this.scrollToBottom();
+    },
+    onStreamError(err) {
+      console.error('SSE Error:', err);
+      this.clearSseTimer();
+      // 已经渲染出内容时不弹错误,避免服务端正常关闭被误判为失败
+      if (!this.mdRawText) {
+        this.errorMsg = 'AI 分析失败,请稍后重试';
+      }
+      this.isloading = false;
+      this.disconnectSSE();
+    },
+    onStreamClose() {
+      // 流正常结束,仅清理超时定时器
+      this.clearSseTimer();
+    },
+    // ===== renderjs → 逻辑层 回调(仅 App 会触发)=====
+    onSseData(data) {
+      this.handleChunk(data);
+    },
+    onSseError(msg) {
+      // renderjs 流式失败(常见于跨域 CORS):若还没收到任何数据,降级到 sse.js 缓冲通道,
+      // 保证至少能出结果(只是没有逐字效果)
+      if (!this._receivedAny && !this._fallbackUsed) {
+        this._fallbackUsed = true;
+        this.stopRenderjs();
+        this.startViaSseJs(this.sseUrl);
+        return;
+      }
+      this.onStreamError(new Error(msg || 'stream error'));
+    },
+    onSseClose() {
+      this.onStreamClose();
+    },
+    disconnectSSE() {
+      if (this.isApp) {
+        this.stopRenderjs();
+      }
+      if (this.sseConnection) {
+        this.sseConnection.close();
+        this.sseConnection = null;
+      }
+    },
+    clearSseTimer() {
+      if (this.sseTimer) {
+        clearTimeout(this.sseTimer);
+        this.sseTimer = null;
+      }
+    },
+    formatTime(date) {
+      return date.toLocaleTimeString([], {
+        hour: '2-digit',
+        minute: '2-digit'
+      });
+    },
+    scrollToBottom() {
+      // 小程序没有 DOM,不能用 $refs.scrollTop;改用 scroll-view 的 scroll-into-view 滚到底部锚点
+      this.scrollIntoView = '';
+      this.$nextTick(() => {
+        this.scrollIntoView = 'chatBottomAnchor';
+      });
+    }
+  },
+  mounted() {
+    this.scrollToBottom();
+    if (this.pestMessage) {
+      this.connectSSE(this.pestMessage);
+    } else {
+      this.isloading = false;
+    }
+  },
+  beforeDestroy() {
+    // 组件卸载时断开连接并清理定时器(uni-app 默认 Vue2,用 beforeDestroy)
+    this.clearSseTimer();
+    this.disconnectSSE();
+  }
+};
+</script>
+
+<script module="sse" lang="renderjs">
+// App 端:在 WebView 层用原生 XMLHttpRequest 接收 SSE 流式数据。
+// 通过 readyState=3 实时读取 responseText 增量并回传逻辑层,实现逐字输出。
+// (plus.net.XMLHttpRequest 会被缓冲、无法实时,故改用 renderjs。)
+export default {
+  data() {
+    return {
+      xhr: null,
+      buffer: '',   // SSE 事件解析缓冲
+      processed: 0  // responseText 已处理长度
+    };
+  },
+  methods: {
+    // 逻辑层改变 sseSignal 时触发(:change:sseSignal="sse.onSignalChange")
+    onSignalChange(signal) {
+      if (!signal) return;
+      if (signal.action === 'start' && signal.url) {
+        this.start(signal.url);
+      } else if (signal.action === 'stop') {
+        this.stop();
+      }
+    },
+    start(url) {
+      this.stop();
+      this.buffer = '';
+      this.processed = 0;
+      const xhr = new XMLHttpRequest();
+      xhr.open('GET', url, true);
+      try { xhr.setRequestHeader('Accept', 'text/event-stream'); } catch (e) {}
+      const self = this;
+      xhr.onreadystatechange = function () {
+        // readyState 3(LOADING) / 4(DONE) 都读取增量,3 时实时推送即为逐字流式
+        if (xhr.readyState === 3 || xhr.readyState === 4) {
+          const full = xhr.responseText || '';
+          if (full.length > self.processed) {
+            const delta = full.substring(self.processed);
+            self.processed = full.length;
+            self.parseAndSend(delta);
+          }
+        }
+        if (xhr.readyState === 4) {
+          self.$ownerInstance.callMethod('onSseClose');
+        }
+      };
+      xhr.onerror = function () {
+        self.$ownerInstance.callMethod('onSseError', 'xhr error');
+      };
+      xhr.send();
+      this.xhr = xhr;
+    },
+    // SSE 协议解析:按空行(\n\n)切事件,提取 data: 字段后回传逻辑层
+    parseAndSend(text) {
+      this.buffer += text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
+      let idx;
+      while ((idx = this.buffer.indexOf('\n\n')) !== -1) {
+        const raw = this.buffer.slice(0, idx);
+        this.buffer = this.buffer.slice(idx + 2);
+        const lines = raw.split('\n');
+        let dataStr = '';
+        for (let i = 0; i < lines.length; i++) {
+          if (lines[i].indexOf('data:') === 0) {
+            dataStr += lines[i].slice(5).replace(/^ /, '');
+          }
+        }
+        if (dataStr !== '') {
+          this.$ownerInstance.callMethod('onSseData', dataStr.replace(/\\n/g, '\n'));
+        }
+      }
+    },
+    stop() {
+      if (this.xhr) {
+        try { this.xhr.abort(); } catch (e) {}
+        this.xhr = null;
+      }
+    }
+  },
+  beforeDestroy() {
+    this.stop();
+  }
+};
+</script>
+
+<style scoped lang="less">
+.el-collapse {
+  background: #f6f6f6;
+  color: #a4aab2;
+  border-radius: 8rpx;
+}
+::v-deep .el-collapse-item__header {
+  padding: 0 16rpx;
+  height: 72rpx;
+  background: #f6f6f6 !important;
+  border-radius: 8rpx;
+}
+::v-deep .el-collapse-item__wrap {
+  background: #f6f6f6 !important;
+  color: #a4aab2 !important;
+  border-radius: 8rpx;
+}
+::v-deep .el-collapse-item__content {
+  color: #a4aab2 !important;
+  border-radius: 8rpx;
+}
+.deep-thinking {
+  padding: 0 52rpx;
+  border-radius: 8rpx;
+}
+.deepseek-chat-container {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  height: calc(100vh - 640rpx);
+  // max-width: 1086px;
+  margin: 0 auto;
+  border-radius: 8rpx;
+  // box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+  font-family: 'Segoe UI', Arial, sans-serif;
+  // background-color: #f9f9f9;
+}
+.chat-messages {
+  flex: 1;
+  overflow-y: auto;
+  height: 100%;
+  // background-color: #ffffff;
+}
+
+.message {
+  display: flex;
+  margin-bottom: 24rpx;
+}
+
+.message-content {
+  max-width: 100%;
+}
+.loading-hint {
+  padding: 16rpx;
+  color: #999;
+  font-size: 26rpx;
+}
+
+.error-hint {
+  color: #e64340;
+}
+
+.message-text {
+  padding: 16rpx;
+  border-radius: 18rpx;
+  line-height: 1.4;
+  word-wrap: break-word;
+  padding-bottom: 60rpx;
+}
+
+.message.bot .message-text {
+  background-color: #fff;
+  border-top-left-radius: 8rpx;
+  color: #5c5c5c;
+}
+
+.message.user .message-text {
+  color: white;
+  border-top-right-radius: 8rpx;
+}
+
+.message-time {
+  font-size: 22rpx;
+  color: #777;
+  margin-top: 8rpx;
+  text-align: right;
+}
+
+.message.user .message-time {
+  text-align: left;
+}
+
+.typing-indicator {
+  display: flex;
+  padding: 12rpx 30rpx;
+  background-color: #f6f6f6;
+  border-radius: 18rpx;
+  border-top-left-radius: 8rpx;
+}
+
+.typing-indicator span {
+  width: 16rpx;
+  height: 16rpx;
+  margin: 0 2rpx;
+  background-color: #999;
+  border-radius: 50%;
+  display: inline-block;
+  animation: bounce 1.4s infinite ease-in-out both;
+}
+
+.typing-indicator span:nth-child(1) {
+  animation-delay: -0.32s;
+}
+
+.typing-indicator span:nth-child(2) {
+  animation-delay: -0.16s;
+}
+@keyframes bounce {
+  0%,
+  80%,
+  100% {
+    transform: scale(0);
+  }
+  40% {
+    transform: scale(1);
+  }
+}
+
+/* Markdown样式 - 使用深度选择器 */
+::v-deep .markdown-output {
+  /* 标题样式 */
+  h1, h2, h3, h4, h5, h6 {
+    margin-top: 32rpx;
+    margin-bottom: 16rpx;
+    font-weight: 600;
+    line-height: 1.25;
+  }
+  h1 { font-size: 1.8em; }
+  h2 { font-size: 1.5em; }
+  h3 { font-size: 1.25em; }
+  h4 { font-size: 1em; }
+  h5 { font-size: 0.875em; }
+  h6 { font-size: 0.85em; }
+
+  /* 列表样式 */
+  ul, ol {
+    padding-left: 2em;
+    margin: 16rpx 0;
+    list-style-type: disc;
+  }
+  ol {
+    list-style-type: decimal;
+  }
+  li {
+    margin: 8rpx 0;
+    line-height: 1.5;
+  }
+
+  /* 粗体斜体 */
+  strong {
+    font-weight: 600;
+  }
+  em {
+    font-style: italic;
+  }
+
+  /* 代码块 */
+  pre {
+    background-color: #f6f8fa;
+    padding: 32rex;
+    border-radius: 12rpx;
+    overflow-x: auto;
+    margin: 16rpx 0;
+  }
+  code {
+    background-color: #f6f8fa;
+    padding: 4rpx 8rpx;
+    border-radius: 6rpx;
+    font-family: monospace;
+  }
+
+  /* 段落 */
+  p {
+    margin: 16rpx 0;
+    line-height: 1.5;
+  }
+}
+
+.message-input {
+  padding: 20rpx 30rpx;
+  border-radius: 8rpx;
+  background-color: #f6f6f6;
+
+  &.footer {
+    position: fixed;
+    bottom: 20rpx;
+    left: 0;
+    right: 0rpx;
+    margin-top: 40rpx;
+    font-size: 24rpx;
+    color: #999;
+    text-align: center;
+  }
+}
+</style>

+ 122 - 0
pages/cbd/components/markdownRenderer.js

@@ -0,0 +1,122 @@
+// markdownRenderer.js
+export function renderMarkdown(raw) {
+  // 过滤结束标记
+  let text = raw.replace(/\[DONE\]/g, '')
+
+  // ── 预处理:在块级标识前插入换行(处理 AI 不换行直接切换块的情况)──
+  // 在 ## 标题前插入换行(前面不是换行时)
+  text = text.replace(/([^\n])(#{1,6} )/g, '$1\n$2')
+  // 在无序列表项前插入换行
+  text = text.replace(/([^\n])([ \t]*[-*+] )/g, '$1\n$2')
+  // 在有序列表项前插入换行
+  text = text.replace(/([^\n])([ \t]*\d+\. )/g, '$1\n$2')
+  // 在引用块前插入换行
+  text = text.replace(/([^\n])(> )/g, '$1\n$2')
+
+  // ── 按段落分割处理(双换行为段落分隔)──
+  // 先把单换行统一,再按块处理
+  const lines = text.split('\n')
+  const blocks = []
+  let i = 0
+
+  while (i < lines.length) {
+    const line = lines[i]
+
+    // 空行跳过
+    if (line.trim() === '') {
+      i++
+      continue
+    }
+
+    // 代码块
+    if (line.startsWith('```')) {
+      const lang = line.slice(3).trim()
+      const codeLines = []
+      i++
+      while (i < lines.length && !lines[i].startsWith('```')) {
+        codeLines.push(lines[i])
+        i++
+      }
+      i++ // 跳过结束 ```
+      const escaped = codeLines.join('\n')
+        .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
+      blocks.push(`<pre><code${lang ? ` class="language-${lang}"` : ''}>${escaped}</code></pre>`)
+      continue
+    }
+
+    // 标题
+    const headingMatch = line.match(/^(#{1,6}) (.+)/)
+    if (headingMatch) {
+      const level = headingMatch[1].length
+      blocks.push(`<h${level}>${applyInline(headingMatch[2])}</h${level}>`)
+      i++
+      continue
+    }
+
+    // 分割线
+    if (/^[-*_]{3,}\s*$/.test(line)) {
+      blocks.push('<hr>')
+      i++
+      continue
+    }
+
+    // 引用块
+    if (line.startsWith('> ') || line === '>') {
+      const content = line.slice(line.startsWith('> ') ? 2 : 1)
+      blocks.push(`<blockquote>${applyInline(content)}</blockquote>`)
+      i++
+      continue
+    }
+
+    // 无序列表:收集连续的列表项
+    if (/^[ \t]*[-*+] /.test(line)) {
+      const items = []
+      while (i < lines.length && /^[ \t]*[-*+] /.test(lines[i])) {
+        const m = lines[i].match(/^[ \t]*[-*+] (.+)/)
+        if (m) items.push(`<li>${applyInline(m[1])}</li>`)
+        i++
+      }
+      blocks.push(`<ul>${items.join('')}</ul>`)
+      continue
+    }
+
+    // 有序列表:收集连续的列表项
+    if (/^[ \t]*\d+\. /.test(line)) {
+      const items = []
+      while (i < lines.length && /^[ \t]*\d+\. /.test(lines[i])) {
+        const m = lines[i].match(/^[ \t]*\d+\. (.+)/)
+        if (m) items.push(`<li>${applyInline(m[1])}</li>`)
+        i++
+      }
+      blocks.push(`<ol>${items.join('')}</ol>`)
+      continue
+    }
+
+    // 普通段落
+    blocks.push(`<p>${applyInline(line)}</p>`)
+    i++
+  }
+
+  return blocks.join('\n')
+}
+
+// 行内样式处理
+function applyInline(text) {
+  // 先转义 HTML 特殊字符,避免 AI 返回的 < > & 破坏 rich-text 渲染;
+  // 转义后再叠加 markdown 标记(标记字符 * _ ` ~ [ ] ( ) 不属于 HTML 特殊字符,不受影响)
+  return escapeHtml(text)
+    .replace(/\*\*\*([^*\n]+?)\*\*\*/g, '<strong><em>$1</em></strong>')
+    .replace(/\*\*([^*\n]+?)\*\*/g, '<strong>$1</strong>')
+    .replace(/\*([^*\n]+?)\*/g, '<em>$1</em>')
+    .replace(/~~([^~\n]+?)~~/g, '<del>$1</del>')
+    .replace(/`([^`\n]+)`/g, '<code>$1</code>')
+    .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
+}
+
+// HTML 转义
+function escapeHtml(str) {
+  return str
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+}

+ 1 - 1
pages/cbd/components/pestArchive.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="pest-archive" v-if="pest_info.name">
+  <view class="pest-archive">
     <view class="pest-archive__header">
       <view class="pest-archive__title">虫害档案</view>
     </view>

+ 21 - 0
pages/cbd/components/pestEchart.vue

@@ -303,6 +303,7 @@ export default {
       const dayData = this.day || [];
       this.dayData = dayData;
       const pestData = this.pest[index];
+      this.updateYAxisSplitNumber(pestData);
       const lineData = {
         categories: dayData,
         series: [
@@ -314,6 +315,26 @@ export default {
       };
       
       this.chartData = lineData;
+    },
+    // 根据数据范围动态计算 y 轴分段数,保证每段刻度间距 >= 1,
+    // 避免数据较小时刻度被取整后出现重复(如 0,0,0,1,1,1)
+    updateYAxisSplitNumber(pestData){
+      const numList = (pestData || [])
+        .map(v => Number(v))
+        .filter(v => !isNaN(v));
+      let dataMin = numList.length ? Math.min(...numList) : 0;
+      let dataMax = numList.length ? Math.max(...numList) : 0;
+      // 与 u-charts 内部逻辑一致:数据全部相同时 min 归 0(全为 0 时 max 取 10)
+      if (dataMin === dataMax) {
+        if (dataMax === 0) {
+          dataMax = 10;
+        }
+        dataMin = 0;
+      }
+      const span = dataMax - dataMin;
+      // 数据范围 >= 5 时固定 5 段(每段间距自然 >= 1);否则按范围取分段数,使每段正好为 1
+      const splitNumber = span >= 5 ? 5 : Math.max(1, Math.floor(span));
+      this.opts.yAxis = { ...this.opts.yAxis, splitNumber };
     }
   }
 };

+ 248 - 0
pages/cbd/components/sse.js

@@ -0,0 +1,248 @@
+// sse.js —— 跨端 SSE 连接
+// H5 / 浏览器:原生 EventSource
+// App(5+):plus.net.XMLHttpRequest 流式读取 responseText 增量
+//           (注意:App 端不支持 uni.request 的 enableChunked / onChunkReceived)
+// 小程序:uni.request 的 enableChunked 分块接收
+
+// ---- UTF-8 解码器:支持跨分片的多字节字符(中文、emoji),避免截断乱码 ----
+// 仅用于小程序 onChunkReceived(拿到的是 ArrayBuffer 字节流)
+function createUtf8Decoder() {
+  let pending = [];
+  return function decode(bytes) {
+    const all = pending.length ? pending.concat(Array.from(bytes)) : Array.from(bytes);
+    let out = '';
+    let i = 0;
+    const len = all.length;
+    while (i < len) {
+      const c = all[i];
+      if (c < 0x80) {
+        out += String.fromCharCode(c);
+        i++;
+      } else if (c >> 5 === 0x06) {
+        // 2 字节
+        if (i + 1 < len) {
+          out += String.fromCharCode(((c & 0x1f) << 6) | (all[i + 1] & 0x3f));
+          i += 2;
+        } else { pending = all.slice(i); return out; }
+      } else if (c >> 4 === 0x0e) {
+        // 3 字节
+        if (i + 2 < len) {
+          out += String.fromCharCode(((c & 0x0f) << 12) | ((all[i + 1] & 0x3f) << 6) | (all[i + 2] & 0x3f));
+          i += 3;
+        } else { pending = all.slice(i); return out; }
+      } else if (c >> 3 === 0x1e) {
+        // 4 字节(代理对)
+        if (i + 3 < len) {
+          let cp = ((c & 0x07) << 18) | ((all[i + 1] & 0x3f) << 12) | ((all[i + 2] & 0x3f) << 6) | (all[i + 3] & 0x3f);
+          cp -= 0x10000;
+          out += String.fromCharCode(0xd800 + (cp >> 10)) + String.fromCharCode(0xdc00 + (cp & 0x3ff));
+          i += 4;
+        } else { pending = all.slice(i); return out; }
+      } else {
+        i++; // 非法字节,跳过
+      }
+    }
+    pending = [];
+    return out;
+  };
+}
+
+// ---- SSE 协议解析:以空行(\n\n)分隔事件,提取 data: 字段 ----
+function createSseParser(onData) {
+  let buffer = '';
+  return function feed(text) {
+    buffer += text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
+    let idx;
+    while ((idx = buffer.indexOf('\n\n')) !== -1) {
+      const raw = buffer.slice(0, idx);
+      buffer = buffer.slice(idx + 2);
+      const lines = raw.split('\n');
+      let dataStr = '';
+      for (const line of lines) {
+        if (line.indexOf('data:') === 0) {
+          dataStr += line.slice(5).replace(/^ /, '');
+        }
+      }
+      if (dataStr !== '') {
+        onData(dataStr);
+      }
+    }
+  };
+}
+
+// 把已转义的 \n 还原(服务端常把换行转义成字面量 "\\n")
+function unescapeData(dataStr) {
+  return dataStr.replace(/\\n/g, '\n');
+}
+
+// H5:原生 EventSource(浏览器自带 SSE 解析,直接拿 data)
+function connectWithEventSource(url, handlers) {
+  const eventSource = new EventSource(url);
+  eventSource.addEventListener('message', (e) => {
+    try {
+      handlers._gotData = true;
+      handlers.onMessage(unescapeData(e.data));
+    } catch (err) {
+      console.error('Error parsing SSE data:', err);
+    }
+  });
+  eventSource.addEventListener('error', (e) => {
+    // EventSource 在服务端正常关闭连接时也会触发 error,区分一下:
+    // 已经收到过数据视为正常结束,没收到过才当作真正的连接失败
+    if (handlers._gotData) {
+      if (typeof handlers.onClose === 'function') handlers.onClose();
+    } else {
+      handlers.onError(e);
+    }
+  });
+  return {
+    close() {
+      try { eventSource.close(); } catch (e) {}
+    }
+  };
+}
+
+// App(5+):plus.net.XMLHttpRequest 流式
+// 通过 onreadystatechange 在 readyState=3/4 时读取 responseText 增量,
+// 某些机型不实时推流时,会在 readyState=4 一次性吐出完整内容(降级但可用)。
+function connectWithPlusXhr(url, handlers) {
+  const feed = createSseParser((dataStr) => {
+    try {
+      handlers._gotData = true;
+      handlers.onMessage(unescapeData(dataStr));
+    } catch (err) {
+      console.error('Error parsing SSE data:', err);
+    }
+  });
+
+  let xhr = null;
+  let processed = 0;
+  let closed = false;
+
+  try {
+    xhr = new plus.net.XMLHttpRequest();
+  } catch (e) {
+    handlers.onError(new Error('当前 App 环境不支持 plus.net.XMLHttpRequest'));
+    return { close() {} };
+  }
+
+  xhr.open('GET', url);
+  try { xhr.setRequestHeader('Accept', 'text/event-stream'); } catch (e) {}
+
+  xhr.onreadystatechange = () => {
+    try {
+      // readyState 3(LOADING) / 4(DONE) 都读取增量
+      if (xhr.readyState === 3 || xhr.readyState === 4) {
+        const full = xhr.responseText || '';
+        if (full.length > processed) {
+          const chunk = full.substring(processed);
+          processed = full.length;
+          feed(chunk);
+        }
+      }
+      if (xhr.readyState === 4) {
+        if (typeof handlers.onClose === 'function') handlers.onClose();
+      }
+    } catch (err) {
+      console.error('plus XHR onreadystatechange error:', err);
+    }
+  };
+  xhr.onerror = (e) => {
+    if (!closed) handlers.onError(e);
+  };
+  xhr.ontimeout = () => {
+    if (!closed) handlers.onError(new Error('请求超时'));
+  };
+
+  try {
+    xhr.send();
+  } catch (e) {
+    handlers.onError(e);
+  }
+
+  return {
+    close() {
+      closed = true;
+      try { xhr && xhr.abort(); } catch (e) {}
+    }
+  };
+}
+
+// 小程序:uni.request 分块流式接收
+function connectWithUniRequest(url, handlers) {
+  const decoder = createUtf8Decoder();
+  const feed = createSseParser((dataStr) => {
+    try {
+      handlers._gotData = true;
+      handlers.onMessage(unescapeData(dataStr));
+    } catch (err) {
+      console.error('Error parsing SSE data:', err);
+    }
+  });
+  let requestTask = null;
+  let closed = false;
+
+  requestTask = uni.request({
+    url,
+    method: 'GET',
+    enableChunked: true,
+    responseType: 'text',
+    success: () => {
+      if (typeof handlers.onClose === 'function') handlers.onClose();
+    },
+    fail: (err) => {
+      if (!closed) handlers.onError(err);
+    }
+  });
+
+  if (requestTask && typeof requestTask.onChunkReceived === 'function') {
+    requestTask.onChunkReceived((res) => {
+      const bytes = new Uint8Array(res.data);
+      const text = decoder(bytes);
+      if (text) feed(text);
+    });
+  } else {
+    handlers.onError(new Error('当前环境不支持流式接收(enableChunked / onChunkReceived)'));
+  }
+
+  return {
+    close() {
+      closed = true;
+      try { requestTask && requestTask.abort && requestTask.abort(); } catch (e) {}
+    }
+  };
+}
+
+// 统一入口:按环境选择连接方式
+export function createSSEConnection(url, options = {}) {
+  const handlers = {
+    onMessage: () => {},
+    onError: () => {},
+    onClose: () => {},
+    _gotData: false
+  };
+
+  let conn;
+  if (typeof EventSource !== 'undefined') {
+    conn = connectWithEventSource(url, handlers);             // H5
+  } else if (typeof plus !== 'undefined' && plus.net && plus.net.XMLHttpRequest) {
+    conn = connectWithPlusXhr(url, handlers);                 // App(5+)
+  } else {
+    conn = connectWithUniRequest(url, handlers);              // 小程序
+  }
+
+  return {
+    onMessage(callback) {
+      handlers.onMessage = callback || (() => {});
+    },
+    onError(callback) {
+      handlers.onError = callback || (() => {});
+    },
+    onClose(callback) {
+      handlers.onClose = callback || (() => {});
+    },
+    close() {
+      conn.close();
+    }
+  };
+}

+ 45 - 5
pages/cbd/detail.vue

@@ -47,6 +47,9 @@
           <view class="tab-item" :class="activeTab === 'pestAnalysis'?'active':''" @click="handleTabClick('pestAnalysis')" v-if="disableShow">
             害虫分析
           </view>
+          <view class="tab-item" :class="activeTab === 'aiAnalysis'?'active':''" @click="handleTabClick('aiAnalysis')" v-if="disableShow">
+            AI智能分析
+          </view>
           <view class="tab-item" :class="activeTab === 'viewImage'?'active':''" @click="handleTabClick('viewImage')">
             查看图片
           </view>
@@ -73,7 +76,7 @@
           </view>
         </view>
       </view>
-      <view v-if="activeTab === 'pestAnalysis'" class="tab-content">
+      <view v-show="activeTab === 'pestAnalysis'" class="tab-content">
         <PestDiscern
           :total="total"
           :pest_order="pest_order"
@@ -105,6 +108,11 @@
           />
         </scroll-view>
       </view>
+      <view v-if="activeTab === 'aiAnalysis'">
+        <AiChat
+          :pestMessage="pestMessage"
+        />
+      </view>
       <view v-show="activeTab === 'deviceData'">
         <DeviceData
           :activeTabLabel="activeTab"
@@ -131,11 +139,13 @@ import PestEchart from './components/pestEchart.vue';
 import PestArchive from './components/pestArchive.vue';
 import photoImage from './components/photoImage.vue';
 import DeviceData from './components/deviceData.vue';
+import AiChat from './components/aiChat.vue';
 
 export default {
   components: {
     DeviceCard,
-    PestDiscern,  
+    PestDiscern,
+    AiChat,
     PestEchart,
     PestArchive,
     photoImage,
@@ -166,6 +176,7 @@ export default {
       mode: 'range',
       imageList: [],
       pest_order:{},
+      pestMessage:'',
       deviceInfo: {},
       pestInfo: {},
       time: '',
@@ -204,7 +215,6 @@ export default {
     } else{
       this.isShowPhoto = true
     }
-    this.getPestAnalysis();
     this.isShow();
     const currentYear = new Date().getFullYear();
     this.selectorRange = [];
@@ -217,6 +227,22 @@ export default {
     }
   },
   methods: {
+    formatePestMessage(data) {
+      if (data.length === 0) {
+        this.pestMessage = '';
+        return;
+      }
+      // 先拼好整段再一次性赋值,避免循环里多次 += 反复触发 pestMessage watcher(连带 AI 连接被反复触发)
+      let msg = `当前设备位置为${this.deviceInfo.address},在${this.startDate}至${this.endDate} 监测到的害虫如下:`;
+      data.forEach((item, index) => {
+        if(index === data.length - 1){
+          msg += `${item.name}:${item.value}头.`;
+        } else {
+          msg += `${item.name}:${item.value}头,`;
+        }
+      });
+      this.pestMessage = msg;
+    },
     handleReachBottom(){
       if (this.activeTab === 'viewImage') {
         if(this.imageList.length >= this.imgTotal){
@@ -285,6 +311,10 @@ export default {
         this.activeTab = 'deviceData';
       }
       this.initAction();
+      // initAction 只在 pestAnalysis 分支取害虫数据;其余初始 tab 也需要 pestMessage 供 AI 分析用,补取一次
+      if (this.activeTab !== 'pestAnalysis') {
+        this.getPestAnalysis();
+      }
       this.isShowTab = showStatus;
       if(this.deviceInfo.disable == 0){
         this.disableShow = false;
@@ -365,6 +395,8 @@ export default {
       this.pest_order = {}
       if(this.activeTab === 'pestAnalysis'){
         this.getPestAnalysis();
+      }else if(this.activeTab === 'aiAnalysis'){
+        this.getAiAnalysis();
       }else if(this.activeTab === 'viewImage'){
         this.initPest();
         this.initImageList();
@@ -374,6 +406,9 @@ export default {
         this.getDeviceHistoryData();
       }
     },
+    getAiAnalysis(){
+
+    },
     handleTabClick(tab) {
       this.activeTab = tab;
       this.initAction();
@@ -482,14 +517,20 @@ export default {
       this.getInfo({});
       let total = 0;
       this.pests = [];
+      const data = [];
       for(let key in pest_order){
         total += pest_order[key] || 0;
         this.pests.push({
           name: key,
           percent: (pest_order[key] / total) * 100
         })
+        data.push({
+          name: key,
+          value: pest_order[key] || 0
+        })
       }
       this.pest_order = pest_order;
+      this.formatePestMessage([...data.reverse()]);
       this.day = res?.day || [];
       const pest = res?.pest || [];
       this.pest = pest;
@@ -583,11 +624,10 @@ export default {
 .device-detail {
   display: flex;
   width: 100%;
-  height: calc(100vh - 112rpx);
+  min-height: calc(100vh - 112rpx);
   padding-top: 112rpx;
   flex-direction: column;
   align-items: center;
-  overflow-y: scroll;
   background: linear-gradient(180deg, #ffffff00 0%, #F5F6FA 23.64%, #F5F6FA 100%), linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%);
   .device-detail__header {
     width: 100%;

+ 121 - 0
pages/deviceDetails/SoilMoisturelist/charts.vue

@@ -0,0 +1,121 @@
+<template>
+  <view>
+    <canvas
+      canvas-id="orIfOOkumJYtFcnuJkjxcySENqRtXceW"
+      id="orIfOOkumJYtFcnuJkjxcySENqRtXceW"
+      class="charts"
+      @touchstart="touchstart"
+      @touchmove="touchmove"
+      @touchend="touchend"
+    />
+  </view>
+</template>
+
+<script>
+import uCharts from "../../../components/js_sdk/u-charts/u-charts/u-charts.js";
+var uChartsInstance = {};
+export default {
+  data() {
+    return {
+      cWidth: 320,
+      cHeight: 200,
+    };
+  },
+  onReady() {
+    //这里的 750 对应 css .charts 的 width
+    // this.cWidth = uni.upx2px(340);
+    //这里的 500 对应 css .charts 的 height
+    // this.cHeight = uni.upx2px(200);
+    this.getServerData();
+  },
+  methods: {
+    getServerData() {
+      //模拟从服务器获取数据时的延时
+      setTimeout(() => {
+        //模拟服务器返回数据,如果数据格式和标准格式不同,需自行按下面的格式拼接
+        let res = {
+          categories: ["2018", "2019", "2020", "2021", "2022", "2023"],
+          series: [
+            {
+              name: "成交量A",
+              linearColor: [
+                [0, "#1890FF"],
+                [0.25, "#00B5FF"],
+                [0.5, "#00D1ED"],
+                [0.75, "#00E6BB"],
+                [1, "#90F489"],
+              ],
+              data: [35, 8, 25, 37, 4, 20],
+            },
+          ],
+        };
+        this.drawCharts("orIfOOkumJYtFcnuJkjxcySENqRtXceW", res);
+      }, 500);
+    },
+    drawCharts(id, data) {
+      const ctx = uni.createCanvasContext(id, this);
+      uChartsInstance[id] = new uCharts({
+        type: "line",
+        context: ctx,
+        width: this.cWidth,
+        height: this.cHeight,
+        categories: data.categories,
+        series: data.series,
+        animation: true,
+        background: "#FFFFFF",
+        color: [
+          "#1890FF",
+          "#91CB74",
+          "#FAC858",
+          "#EE6666",
+          "#73C0DE",
+          "#3CA272",
+          "#FC8452",
+          "#9A60B4",
+          "#ea7ccc",
+        ],
+        padding: [10, 10, 10, 10],
+        enableScroll: true,
+        legend: {
+          position: "top",
+        },
+        xAxis: {
+          disableGrid: true,
+          scrollShow: true,
+          itemCount: 4,
+        },
+        yAxis: {
+          gridType: "dash",
+          dashLength: 2,
+        },
+        dataLabel: false,
+        extra: {
+          line: {
+            type: "curve",
+            width: 2,
+            activeType: "hollow",
+          },
+        },
+      });
+    },
+    touchstart(e) {
+      uChartsInstance[e.target.id].scrollStart(e);
+    },
+    touchmove(e) {
+      uChartsInstance[e.target.id].scroll(e);
+    },
+    touchend(e) {
+      uChartsInstance[e.target.id].scrollEnd(e);
+      uChartsInstance[e.target.id].touchLegend(e);
+      uChartsInstance[e.target.id].showToolTip(e);
+    },
+  },
+};
+</script>
+
+<style scoped>
+.charts {
+  width: 750rpx;
+  height: 500rpx;
+}
+</style>

+ 148 - 0
pages/deviceDetails/SoilMoisturelist/devControl.vue

@@ -0,0 +1,148 @@
+<template>
+  <view class="device-details-page">
+    <cu-custom :isBack="true">
+      <template slot="content">
+        <view class="nav-title">参数设置</view>
+      </template>
+    </cu-custom>
+    <view class="control-board">
+      <view class="title">操作</view>
+      <text style="color:red">注:步长值最小值10cm、最大值200cm且为整数</text>
+      <u-form :model="form" ref="uForm" label-width="200rpx">
+        <u-form-item label="第1层步长值">
+          <view style="display:flex;justify-content: space-between">
+            <u-input v-model="form.depth[0]" max="200" min="10" maxlength="3" />cm
+          </view>
+        </u-form-item>
+        <u-form-item label="第2层步长值">
+          <view style="display:flex;justify-content: space-between;">
+          <u-input v-model="form.depth[1]" max="200" min="10" maxlength="3" />cm
+          </view>
+        </u-form-item>
+        <u-form-item label="第3层步长值">
+          <view style="display:flex;justify-content: space-between">
+            <u-input v-model="form.depth[2]" max="200" min="10" maxlength="3" />cm
+          </view>
+        </u-form-item>
+      </u-form>
+       <u-button type="success" class="sub-btn" @click="submitControlParams()">确定</u-button>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  onLoad(options) {
+    const deviceInfo = JSON.parse(decodeURIComponent(options.deviceInfo));
+    this.deviceInfo = deviceInfo;
+    this.getDevControlParams();
+  },
+  data() {
+    return {
+      deviceInfo: {},
+      form: {
+        depth: [],
+      },
+    };
+  },
+  methods: {
+    async getDevControlParams() {
+      await this.$myRequest({
+        url: '/api/api_gateway?method=weather.weather.nd_topic_change',
+        data: {
+          device_id: this.deviceInfo.devBid,
+        }
+      }).then(res => {
+        const depth = res.depth.split(',') || [];
+        this.form.depth = depth
+      })
+    },
+    async submitControlParams(){
+      if(this.form.depth.length !== 3){
+        uni.showToast({
+          title: "请输入3个步长值",
+          icon: "none",
+        });
+        return
+      }
+      if(!this.form.depth.every(item => item >= 10 && item <= 200 && Number.isInteger(Number(item)))){
+        uni.showToast({
+          title: "请输入正确的步长值",
+          icon: "none",
+        });
+        return
+      }
+      await this.$myRequest({
+        url: '/api/api_gateway?method=weather.weather.nd_topic_change',
+        data: {
+          device_id: this.deviceInfo.devBid,
+          depth: this.form.depth.join(','),
+        }
+      }).then(res => {
+        uni.showToast({
+          title: "操作成功",
+          icon: "none",
+        });
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.device-details-page {
+  background: linear-gradient(
+      180deg,
+      #ffffff00 0%,
+      #f5f6fa 23.64%,
+      #f5f6fa 100%
+    ),
+    linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%);
+  min-height: 100vh;
+  padding: 0 32rpx;
+  padding-top: 98rpx;
+}
+.nav-title{
+  text-align: center;
+}
+.control-board {
+  border-radius: 16rpx;
+  background: #fff;
+  padding: 32rpx;
+  margin-top: 46rpx;
+  .title {
+    margin-bottom: 16rpx;
+    color: #303133;
+    font-size: 28rpx;
+    font-weight: 400;
+  }
+  .date-title {
+    margin-bottom: 52rpx;
+  }
+  .reload-btn {
+    width: 100%;
+    background: #0bbc58;
+    margin-bottom: 48rpx;
+    border-radius: 16rpx;
+  }
+  .date-slider {
+    
+  }
+}
+
+.footer-board {
+  border-top: 2rpx solid #e4e7ed;
+  background: #fff;
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 164rpx;
+  padding: 16rpx 32rpx 0 32rpx;
+  .sub-btn {
+    width: 100%;
+    background: #0bbc58;
+    border-radius: 16rpx;
+  }
+}
+</style>

+ 350 - 0
pages/deviceDetails/SoilMoisturelist/devRepair.vue

@@ -0,0 +1,350 @@
+<template>
+  <view class="device-repair-page">
+    <cu-custom :isBack="true">
+      <template slot="content">
+        <view class="nav-title">设备报修</view>
+      </template>
+    </cu-custom>
+    <view class="form-board">
+      <u-form
+        ref="baseFormRef"
+        :model="baseForm"
+        labelWidth="162"
+        :rules="rules"
+        errorType="toast"
+      >
+        <view class="repaid-board">
+          <view class="title">基本信息</view>
+          <u-form-item label="设备ID" prop="device_id" required>
+            <u-input
+              v-model="baseForm.device_id"
+              placeholder="设备ID"
+              disabled
+            ></u-input>
+          </u-form-item>
+          <u-form-item label="设备名称" prop="devName" required>
+            <u-input
+              v-model="baseForm.devName"
+              placeholder="请输入设备名称"
+              disabled
+            ></u-input>
+          </u-form-item>
+          <u-form-item label="联系人" prop="user" required>
+            <u-input
+              v-model="baseForm.user"
+              placeholder="请输入"
+            ></u-input>
+          </u-form-item>
+          <u-form-item label="联系人电话" prop="userphone" required>
+            <u-input
+              v-model="baseForm.userphone"
+              placeholder="请输入"
+              type="number"
+            ></u-input>
+          </u-form-item>
+          <u-form-item label="联系人地址" prop="addr" required>
+            <u-input
+              v-model="baseForm.addr"
+              placeholder="请输入"
+            ></u-input>
+          </u-form-item>
+        </view>
+        <view class="repaid-board">
+          <view class="title">报修详情</view>
+          <u-input
+            v-model="baseForm.errordesc"
+            placeholder="请描述设备问题"
+            type="textarea"
+            :height="216"
+            maxlength="140"
+            show-word-limit
+          ></u-input>
+
+          <view class="upload-section">
+            <view class="upload-title"
+              >设备照片<text class="info">(最多4张)</text></view
+            >
+            <u-upload
+              :file-list="fileList"
+              :max-count="4"
+              @on-choose-complete="onChooseComplete"
+              @on-remove="onRemove"
+              name="upload"
+              :max-size="5 * 1024 * 1024"
+              accept="image/*"
+              :auto-upload="false"
+              :show-progress="false"
+            >
+              <view slot="addBtn" class="add-btn">
+                <u-icon name="plus" size="56" color="#909399"></u-icon>
+              </view>
+            </u-upload>
+          </view>
+        </view>
+      </u-form>
+    </view>
+
+    <view class="footer-board">
+      <u-button type="success" class="sub-btn" @click="submitForm"
+        >确定</u-button
+      >
+    </view>
+  </view>
+</template>
+
+<script>
+import uInput from "../../../uview-ui/components/u-input/u-input.vue";
+export default {
+  components: { uInput },
+  onLoad(options) {
+    const deviceInfo = JSON.parse(decodeURIComponent(options.deviceInfo));
+    console.log("deviceInfo:", deviceInfo);
+    this.baseForm.device_id = deviceInfo.devBid
+    this.baseForm.devName = deviceInfo.devName
+  },
+  data() {
+    return {
+      baseForm: {
+        device_id: "",
+        devName: "",
+        user: "",
+        userphone: "",
+        addr: "",
+        errordesc: "",
+      },
+      rules: {
+        devName: [
+          {
+            required: true,
+            message: "请输入设备名称",
+            trigger: ["blur", "change"],
+          },
+        ],
+        user: [
+          {
+            required: true,
+            message: "请输入联系人",
+            trigger: ["blur", "change"],
+          },
+        ],
+        userphone: [
+          {
+            required: true,
+            message: "请输入联系人电话",
+            trigger: ["blur", "change"],
+          },
+          {
+            pattern: /^1[3-9]\d{9}$/,
+            message: "请输入正确的手机号",
+            trigger: ["blur", "change"],
+          },
+        ],
+        addr: [
+          {
+            required: true,
+            message: "请输入联系人地址",
+            trigger: ["blur", "change"],
+          },
+        ],
+        errordesc: [
+          {
+            required: true,
+            message: "请描述设备问题",
+            trigger: ["blur", "change"],
+          },
+        ],
+      },
+      fileList: [],
+    };
+  },
+  methods: {
+    onChooseComplete(lists) {
+      const newFiles = lists.filter(
+        (item) => !this.fileList.some((f) => f.url === item.url)
+      );
+      newFiles.forEach((file) => {
+        const item = {
+          url: file.url,
+          status: "uploading",
+        };
+        this.fileList.push(item);
+        const index = this.fileList.length - 1;
+        uni.uploadFile({
+          url: "https://wx.hnyfwlw.com/api/api_gateway?method=base.bases.base_photo",
+          filePath: file.url,
+          name: "img_file",
+          formData: {
+            user: "test",
+          },
+          success: (uploadFileRes) => {
+            const data = JSON.parse(uploadFileRes.data);
+            if (data.data && data.data.src) {
+              this.fileList[index].status = "success";
+              this.fileList[index].src = data.data.src;
+              this.$forceUpdate();
+              console.log("上传成功,图片地址:", data.data.src);
+            } else {
+              this.fileList[index].status = "failed";
+              uni.showToast({
+                title: data.msg || "上传失败",
+                icon: "none",
+              });
+            }
+          },
+          fail: (err) => {
+            this.fileList[index].status = "failed";
+            uni.showToast({
+              title: "上传失败",
+              icon: "none",
+            });
+            console.error("上传失败:", err);
+          },
+        });
+      });
+    },
+    onRemove(index) {
+      this.fileList.splice(index, 1);
+    },
+    submitForm() {
+      const hasUploading = this.fileList.some((f) => f.status === "uploading");
+      if (hasUploading) {
+        uni.showToast({
+          title: "请等待图片上传完成",
+          icon: "none",
+        });
+        return;
+      }
+      const hasFailed = this.fileList.some((f) => f.status === "failed");
+      if (hasFailed) {
+        uni.showToast({
+          title: "有图片上传失败,请重新上传",
+          icon: "none",
+        });
+        return;
+      }
+      this.$refs.baseFormRef
+        .validate()
+        .then((res) => {
+          const uploadedImages = this.fileList
+            .filter((f) => f.status === "success" && f.src)
+            .map((f) => f.src);
+          const params = {
+            ...this.baseForm,
+            errorimg: uploadedImages,
+          };
+          console.log("提交参数:", params);
+          // 
+          this.$myRequest({
+            url: "/api/api_gateway?method=after_sale.after_sale_manage.aftersale_apply",
+            data: params,
+          }).then(res => {
+            console.log("aftersale_apply:", res);
+            if(res){
+              uni.showToast({
+                  title: '提交成功',
+                  duration: 2000,
+              });
+              setTimeout(()=>{
+                uni.navigateBack({
+                  delta:1
+                })
+              },1000)
+            }else{
+              uni.showToast({
+                  title: '提交失败',
+                  duration: 2000,
+                icon:"none"
+              });
+            }
+          })
+        })
+        .catch((errors) => {
+          console.log("表单验证失败", errors);
+        });
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.device-repair-page {
+  background:
+    linear-gradient(180deg, #ffffff00 0%, #f5f6fa 23.64%, #f5f6fa 100%),
+    linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%);
+  min-height: 100vh;
+  padding: 0 32rpx;
+  padding-top: 98rpx;
+  padding-bottom: 200rpx;
+}
+
+.form-board {
+  margin-top: 46rpx;
+}
+.repaid-board {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 32rpx;
+  margin-bottom: 24rpx;
+
+  .title {
+    color: #020305;
+    font-size: 32rpx;
+    font-weight: bold;
+  }
+}
+
+.upload-section {
+  padding: 20rpx 0;
+
+  .upload-title {
+    font-size: 28rpx;
+    color: #303133;
+    margin-bottom: 16rpx;
+    .info {
+      color: #999;
+    }
+  }
+
+  .add-btn {
+    background: #f1f4f8;
+    width: 160rpx;
+    height: 160rpx;
+    border-radius: 12rpx;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+}
+
+// 使所有输入框文本右对齐
+::v-deep .uni-input-input,
+::v-deep .uni-input-placeholder {
+  text-align: right !important;
+}
+::v-deep .u-add-wrap {
+  display: none !important;
+}
+::v-deep .u-input__textarea {
+  background: #f1f4f8 !important;
+  border-radius: 8rpx;
+  margin-top: 24rpx;
+  padding: 17rpx 24rpx !important;
+}
+
+.footer-board {
+  border-top: 2rpx solid #e4e7ed;
+  background: #fff;
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 164rpx;
+  padding: 16rpx 32rpx 0 32rpx;
+
+  .sub-btn {
+    width: 100%;
+    background: #0bbc58;
+    border-radius: 16rpx;
+  }
+}
+</style>

+ 899 - 0
pages/deviceDetails/SoilMoisturelist/eleDetail.vue

@@ -0,0 +1,899 @@
+<template>
+  <view class="ele-details-page">
+    <cu-custom :isBack="true">
+      <template slot="content">
+        <view class="nav-title">气象/墒情监测</view>
+      </template>
+    </cu-custom>
+    <view class="date-picker" @click="openDatePicker">
+      <text>{{ startDate }}</text>
+      <text class="separator">-</text>
+      <text>{{ endDate }}</text>
+      <image class="date-icon" src="/static/images/device/date-active.svg" />
+    </view>
+    <u-calendar
+      v-model="show"
+      :mode="mode"
+      @change="onDateChange"
+      ref="calendar" 
+      range-color="#999"
+      btn-type="success"
+      active-bg-color="#0BBC58"
+      range-bg-color="rgba(11,188,88,0.13)"
+    ></u-calendar>
+    <view class="echarts-board">
+      <view class="period-tabs">
+        <view
+          class="tab-item"
+          :class="{ active: activePeriod === '24h' }"
+          @click="changePeriod('24h')"
+          >24小时</view
+        >
+        <view
+          class="tab-item"
+          :class="{ active: activePeriod === '7d' }"
+          @click="changePeriod('7d')"
+          >一周</view
+        >
+        <view
+          class="tab-item"
+          :class="{ active: activePeriod === '30d' }"
+          @click="changePeriod('30d')"
+          >一月</view
+        >
+      </view>
+
+      <view class="element-tabs" v-if="hasData">
+        <view class="ele-board">
+          <view
+            v-for="(item, index) in eleTabsList"
+            :key="index"
+            class="ele-item"
+            :class="{ 'ele-active': eleActive === index }"
+            @click="changeElement(index,item)"
+            >{{ item.name }}</view
+          >
+        </view>
+        <view class="fold-icon" @click="toggleFold">
+          <u-icon :name="isFolded ? 'arrow-down' : 'arrow-up'"></u-icon>
+        </view>
+      </view>
+      <!-- 展开后的所有要素 -->
+      <view v-if="!isFolded && hasData" class="expanded-elements">
+        <view
+          class="expanded-item"
+          v-for="(item, index) in eleTabsList"
+          :key="index"
+          :class="{ 'ele-active': eleActive === index }"
+          @click="changeElement(index,item)"
+        >
+          {{ item.name }}
+        </view>
+      </view>
+
+      <view class="chart-container" v-if="hasData">
+        <canvas
+          v-show="!show"
+          canvas-id="temperatureChart"
+          id="temperatureChart"
+          class="chart"
+          @touchstart="touchLineA($event)"
+          @touchmove="moveLineA($event)"
+          @touchend="touchEndLineA($event)"
+          disable-scroll="true"
+        ></canvas>
+      </view>
+      <view class="no-data" v-else>
+        <text>暂无数据</text>
+      </view>
+
+      <!-- <view class="stats-panel">
+        <view class="stat-item">
+          <text class="value">27<text class="unit">°C</text></text>
+          <text class="label">最低值</text>
+        </view>
+        <view class="stat-item">
+          <text class="value">32<text class="unit">°C</text></text>
+          <text class="label">平均值</text>
+        </view>
+        <view class="stat-item">
+          <text class="value red">42<text class="unit red">°C</text></text>
+          <text class="label">最高值</text>
+        </view>
+      </view> -->
+    </view>
+
+    <view class="history-section">
+      <view class="section-title">历史数据</view>
+      <view class="table-wrap">
+        <view class="fixed-column">
+          <view class="table-cell header">上报时间</view>
+          <view class="table-cell" v-for="(item, idx) in tableList" :key="idx">
+            {{ formatTimestamp(item.time) }}
+          </view>
+        </view>
+        <view class="table-bg"></view>
+        <scroll-view class="scroll-column" scroll-x="true">
+          <view class="scroll-content">
+            <view class="table-row">
+              <view class="table-cell header">{{
+                eleKey + "(" + unit + ")"
+              }}</view>
+            </view>
+            <view class="table-row" v-for="(item, idx) in tableList" :key="idx">
+              <view class="table-cell">{{ item.value }}</view>
+            </view>
+          </view>
+        </scroll-view>
+      </view>
+
+      <view class="pagination">
+        <view
+          class="page-item prev"
+          @click="prevPage"
+          :class="{ disabled: currentPage === 1 }"
+          >上一页</view
+        >
+        <view class="page-info">
+          <text class="curret-page">{{ currentPage }}</text>
+          <text>/</text>
+          <text>{{ totalPages }}</text>
+        </view>
+        <view
+          class="page-item next"
+          @click="nextPage"
+          :class="{ disabled: currentPage >= totalPages }"
+          >下一页</view
+        >
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import uCharts from "../../../components/js_sdk/u-charts/u-charts/u-charts.js";
+import baseCharts from "./charts.vue";
+export default {
+  components: { baseCharts },
+  onLoad(options) {
+    const deviceInfo = JSON.parse(decodeURIComponent(options.deviceInfo));
+    this.eleKey = options.eleKey || "";
+    this.unit = options.unit || "";
+    this.key = options.key || "";
+    this.initDateRange(deviceInfo.uptime);
+    this.deviceInfo = deviceInfo;
+    this.getChartData();
+    this.getObData();
+  },
+  data() {
+    return {
+      unit: "",
+      key: "",
+      temperatureChart: null,
+      activePeriod: "",
+      activeElement: "temperature",
+      show: false,
+      mode: "range",
+      startDate: "",
+      endDate: "",
+      isFolded: true, // 控制要素是否收起
+      eleTabs: [],
+      eleTabsList: [],
+      eleActive: 0,
+      hasData: true,
+      tableList: [],
+      tableAllList: [],
+      titles: [],
+      currentPage: 1,
+      pageSize: 9,
+      total: 0,
+      chartData: {
+        categories: [],
+        series: [],
+      },
+      activeChartInfo: {},
+      eleKey: "",
+    };
+  },
+  computed: {
+    totalPages() {
+      return Math.ceil(this.total / this.pageSize);
+    },
+  },
+  methods: {
+    formatTimestamp(timestamp) {
+      if (!timestamp) return '';
+      const date = new Date(Number(timestamp) * 1000);
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, '0');
+      const day = String(date.getDate()).padStart(2, '0');
+      const hours = String(date.getHours()).padStart(2, '0');
+      const minutes = String(date.getMinutes()).padStart(2, '0');
+      const seconds = String(date.getSeconds()).padStart(2, '0');
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+    },
+    initDateRange(timestamp) {
+      if (!timestamp) {
+        const today = new Date();
+        this.endDate = this.formatDate(today);
+        const startDate = new Date(today);
+        startDate.setDate(startDate.getDate() - 7);
+        this.startDate = this.formatDate(startDate);
+        return;
+      }
+      const date = new Date(Number(timestamp) * 1000);
+      this.endDate = this.formatDate(date);
+      const startDate = new Date(date);
+      startDate.setDate(startDate.getDate() - 7);
+      this.startDate = this.formatDate(startDate);
+    },
+    formatDate(date) {
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, "0");
+      const day = String(date.getDate()).padStart(2, "0");
+      return `${year}-${month}-${day}`;
+    },
+    initChart() {
+      const ctx = uni.createCanvasContext("temperatureChart", this);
+      const currentEle = this.eleTabs.get(this.key);
+      // u-charts 的 series.data 必须是数字数组;原数据 value 为字符串(含 "/"、"NA"),
+      // 直接传对象数组会导致 Y 轴 min/max 计算异常,刻度全部相同
+      const seriesData = currentEle.map((item) => {
+        const val = item.value;
+        if (val === "/" || val === "NA" || val === null || val === undefined || val === "") {
+          return 0;
+        }
+        const num = Number(val);
+        return isNaN(num) ? 0 : num;
+      });
+      const series = [
+        {
+          name: currentEle[0].name,
+          data: seriesData,
+          lineWidth: 3,
+          pointShape: "circle",
+          pointSize: 12,
+          pointColor: currentEle.color,
+          pointBorderColor: "#fff",
+          pointBorderWidth: 2,
+          symbol: "emptyCircle",
+          symbolSize: 6,
+          legendShape: "circle",
+        },
+      ];
+      const xData = currentEle.map((item) => item.time);
+
+      this.temperatureChart = new uCharts({
+        $this: this,
+        canvasId: "temperatureChart",
+        type: "line",
+        context: ctx,
+        width: 340,
+        height: 180,
+        categories: xData,
+        series: series,
+        legend: {
+          position: "top",
+          show: true,
+          padding: 5,
+          fontColor: "#303133",
+          fontSize: 12,
+          itemType: "circle",
+          itemShape: "emptyCircle",
+          itemWidth: 12,
+          itemHeight: 12,
+          lineWidth: 2,
+        },
+        xAxis: {
+          axisLineColor: "#E4E7ED",
+          disableGrid: true,
+          fontColor: "#656565",
+          fontSize: 10,
+          itemCount: 5,
+          scrollShow: true,
+        },
+        yAxis: {
+          tofix: 2,
+          axisLineColor: "#E4E7ED",
+          fontColor: "#656565",
+          gridColor: "#E4E7ED",
+          dashLength: 4,
+          disableGrid: false,
+          fontSize: 10,
+        },
+        dataLabel: false,
+        enableScroll: true,
+        background: "#FFFFFF",
+        extra: {
+          line: {
+            type: "curve",
+            width: 2,
+            activeType: "hollow",
+          },
+        },
+      });
+    },
+    touchLineA(e) {
+      this.temperatureChart.scrollStart(e);
+    },
+    moveLineA(e) {
+      this.temperatureChart.scroll(e);
+    },
+    touchEndLineA(e) {
+      this.temperatureChart.scrollEnd(e);
+      //下面是toolTip事件,如果滚动后不需要显示,可不填写
+      // this.temperatureChart.showToolTip(e, {
+      //   format: function(item, category) {
+      //     console.log(item, 'iyee', e)
+      //     return category + ' ' + item.name + ':' + item.data
+      //   }
+      // });
+    },
+
+    // 打开日期选择器
+    openDatePicker() {
+      this.show = true;
+      // 延迟设置日期,确保组件已经初始化
+      this.$nextTick(() => {
+        if (this.$refs.calendar) {
+          // 直接设置组件内部的日期状态
+          const calendar = this.$refs.calendar;
+          calendar.startDate = this.startDate;
+          calendar.endDate = this.endDate;
+
+          // 如果有选中的日期范围,设置对应的年、月、日
+          if (this.startDate) {
+            const startParts = this.startDate.split("-");
+            calendar.startYear = parseInt(startParts[0]);
+            calendar.startMonth = parseInt(startParts[1]);
+            calendar.startDay = parseInt(startParts[2]);
+          }
+          if (this.endDate) {
+            const endParts = this.endDate.split("-");
+            calendar.endYear = parseInt(endParts[0]);
+            calendar.endMonth = parseInt(endParts[1]);
+            calendar.endDay = parseInt(endParts[2]);
+          }
+
+          // 如果有开始日期,设置日历当前显示的年月为开始日期的年月
+          if (this.startDate) {
+            const startParts = this.startDate.split("-");
+            calendar.year = parseInt(startParts[0]);
+            calendar.month = parseInt(startParts[1]);
+            calendar.changeData();
+          }
+        }
+      });
+    },
+
+    // 日期选择变化事件
+    onDateChange(e) {
+      this.startDate = e.startDate;
+      this.endDate = e.endDate;
+      this.activePeriod = "custom"; // 设置为自定义周期
+      this.show = false;
+      this.getChartData();
+      this.getObData();
+    },
+
+    // 切换时间周期
+    changePeriod(period) {
+      this.activePeriod = period;
+
+      // 根据选择的周期自动计算日期范围
+      const now = new Date();
+      const endDate = now.toISOString().split("T")[0];
+      const startDate = new Date();
+
+      if (period === "24h") {
+        startDate.setDate(now.getDate());
+      } else if (period === "7d") {
+        startDate.setDate(now.getDate() - 7);
+      } else if (period === "30d") {
+        startDate.setMonth(now.getMonth() - 1);
+      }
+
+      this.startDate = startDate.toISOString().split("T")[0];
+      this.endDate = endDate;
+      this.getChartData();
+      this.getObData();
+    },
+
+    // 切换要素标签
+    changeElement(index,item) {
+      if (this.eleTabs.length === 0) return;
+      this.eleActive = index;
+      this.activeChartInfo = this.eleTabs[index];
+      this.key = item.key
+      this.isFolded = true; // 关闭展开面板
+      const newData = [];
+      this.eleKey = item.name
+      this.unit = item.unit
+      this.tableAllList.forEach(item => {
+        const dat = item?.dat || {};
+        for(let key in dat){
+          if(key == this.key){
+            const value = dat[key] ? dat[key]?.split('#')?.[0] : 'N/A';
+            newData.push({
+              time: item.time,
+              value,
+            })
+          }
+        }
+      })
+      this.tableList = newData;
+      this.$nextTick(() => {
+        this.initChart();
+      });
+    },
+    getKeyByName(key){
+      const titles = [...this.titles]
+      let k
+      titles.forEach(title => {
+        if(title.name === key){
+          k = title.key
+        }
+      })
+      return k
+    },
+    // 切换要素展开/收起状态
+    toggleFold() {
+      this.isFolded = !this.isFolded;
+    },
+    // 上一页
+    prevPage() {
+      if (this.currentPage > 1) {
+        this.currentPage--;
+        this.getObData();
+      }
+    },
+    // 下一页
+    nextPage() {
+      if (this.currentPage < this.totalPages) {
+        this.currentPage++;
+        this.getObData();
+      }
+    },
+    // 折线图数据
+    getChartData() {
+      this.$myRequest({
+        url: "/api/api_gateway?method=weather.weather.qxz_data_chart",
+        data: {
+          device_id: this.deviceInfo.devBid,
+          begin: String(
+            Math.floor(new Date(this.startDate + " 00:00:00").getTime() / 1000),
+          ),
+          end: String(
+            Math.floor(new Date(this.endDate + " 23:59:59").getTime() / 1000),
+          ),
+        },
+      }).then((res) => {
+        console.log("getChartData:", res);
+        const resList = res?.data || []
+        const conf = res?.conf || {}
+        if (resList && Array.isArray(resList) && resList.length > 0) {
+          const map = new Map()
+          resList.forEach(item => {
+            const dat = item?.dat[1] || {};
+            for(let key in dat){
+              if(dat[key]){
+              const name = conf[key].split('#')[0]
+                if(map.has(key)){
+                  map.get(key).push({
+                    time: this.formatTime(item.time*1000,'yyyy-MM-dd'),
+                    value: dat[key],
+                    name,
+                    key
+                  })
+                }else{
+                  map.set(key, [{
+                    time: this.formatTime(item.time*1000,'yyyy-MM-dd'),
+                    value: dat[key],
+                    name,
+                    key
+                    }]
+                  )
+                }
+              }
+            }
+          })
+          this.eleTabs = map;
+          this.eleTabsList = [];
+          map.forEach((value, key) => {
+            this.eleTabsList.push({
+              name: conf[key].split('#')[0],
+              key,
+              unit: conf[key].split('#')[1],
+              value: value,
+            })
+          })
+          this.hasData = true;
+          this.$nextTick(() => {
+            this.initChart();
+          });
+        } else {
+          this.eleTabs = [];
+          this.hasData = false;
+        }
+      });
+    },
+    // 历史数据
+    getObData() {
+      this.$myRequest({
+        url: "/api/api_gateway?method=weather.weather.qxz_detail",
+        data: {
+          device_id: this.deviceInfo.devBid,
+          start_time: String(
+            Math.floor(new Date(this.startDate + " 00:00:00").getTime() / 1000),
+          ),
+          end_time: String(
+            Math.floor(new Date(this.endDate + " 23:59:59").getTime() / 1000),
+          ),
+          page: this.currentPage,
+          page_size: this.pageSize,
+        },
+      }).then((res) => {
+        console.log("getObData:", res);
+        const data = res.data || [];
+        const newData = [];
+        this.tableAllList = data;
+        data.forEach(item => {
+          const dat = item?.dat || {};
+          for(let key in dat){
+            if(key == this.key){
+              newData.push({
+                time: item.time,
+                value: dat[key].split('#')[0],
+              })
+            }
+          }
+        })
+        this.titles = res?.title || [];
+        this.tableList = newData;
+        this.total = res.nums;
+      });
+    },
+    convertDynamic(data, getLabelValue) {
+      if (!data || typeof data !== "object") return [];
+      const result = [];
+      const keys = Object.keys(data);
+      for (const prop of keys) {
+        const arr = data[prop];
+        const value = getLabelValue(prop, arr, data);
+        if (value !== undefined && value !== null) {
+          result.push({
+            label: String(value),
+            prop,
+            align: "center",
+          });
+        }
+      }
+      return result;
+    },
+    processData(data) {
+      return Object.entries(data).reduce((acc, [key, value]) => {
+        acc[key] = Array.isArray(value) ? value[0] : value;
+        return acc;
+      }, {});
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.ele-details-page {
+  background:
+    linear-gradient(180deg, #ffffff00 0%, #f5f6fa 23.64%, #f5f6fa 100%),
+    linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%);
+  min-height: 100vh;
+  padding: 0 32rpx;
+  padding-top: 98rpx;
+}
+.nav-title{
+  text-align: center;
+}
+.date-picker {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background-color: #fff;
+  border-radius: 40rpx;
+  padding: 16rpx 24rpx;
+  margin-bottom: 36rpx;
+  font-size: 28rpx;
+  color: #999;
+  margin-top: 46rpx;
+  .date-icon {
+    width: 32rpx;
+    height: 32rpx;
+  }
+}
+
+.separator {
+  margin: 0 16rpx;
+}
+.echarts-board {
+  padding: 32rpx;
+  border-radius: 16rpx;
+  background: #fff;
+  margin-bottom: 24rpx;
+  position: relative;
+  .period-tabs {
+    width: 350rpx;
+    height: 64rpx;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin: auto;
+    margin-bottom: 12rpx;
+    border-radius: 32rpx;
+    background: #f1f4f8;
+    padding: 8rpx 10rpx;
+    color: #303133;
+    font-family: "Source Han Sans CN VF";
+    font-size: 26rpx;
+    font-style: normal;
+    font-weight: 400;
+    line-height: normal;
+    .tab-item {
+      padding: 5rpx 24rpx;
+      height: 48rpx;
+      line-height: 48rpx;
+      border-radius: 32rpx;
+    }
+    .active {
+      background: #fff;
+    }
+  }
+  .element-tabs {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    overflow-x: auto;
+    white-space: nowrap;
+    .ele-board {
+      flex: 1;
+      display: flex;
+      align-items: center;
+      overflow-x: auto;
+      overflow-y: hidden;
+      white-space: nowrap;
+    }
+    .ele-item {
+      position: relative;
+      color: #999999;
+      font-size: 28rpx;
+      font-weight: 400;
+      white-space: nowrap;
+      margin-right: 16rpx;
+      padding: 14rpx 16rpx;
+      cursor: pointer;
+    }
+    .ele-active {
+      color: #303133;
+      font-weight: 500;
+      &::after {
+        content: "";
+        position: absolute;
+        left: 50%;
+        transform: translateX(-50%);
+        bottom: 4rpx;
+        display: block;
+        width: 30rpx;
+        height: 8rpx;
+        border-radius: 4rpx;
+        background: #1fc676;
+      }
+    }
+    .fold-icon {
+      width: 48rpx;
+      border-left: 2px solid #999;
+      padding-left: 16rpx;
+      margin-left: 16rpx;
+      cursor: pointer;
+      display: none;
+    }
+  }
+  /* 展开后的要素容器样式 */
+  .expanded-elements {
+    position: absolute;
+    top: 126rpx;
+    left: 0;
+    right: 0;
+    background: #f5f7fa;
+    border-radius: 12rpx;
+    padding: 16rpx;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 16rpx;
+    z-index: 10;
+    box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
+  }
+
+  .expanded-item {
+    flex: 0 0 calc(33.33% - 16rpx);
+    text-align: center;
+    padding: 16rpx 0;
+    border-radius: 8rpx;
+    color: #999999;
+    font-size: 26rpx;
+    background: #ffffff;
+    cursor: pointer;
+    transition: all 0.3s ease;
+  }
+
+  .expanded-item.ele-active {
+    color: #1fc676;
+    background: #e6f7ef;
+    font-weight: 500;
+  }
+
+  .chart-container {
+    border-radius: 16rpx;
+    margin-bottom: 24rpx;
+    position: relative;
+    padding: 10rpx;
+    overflow: hidden;
+    height: 336rpx;
+    z-index:0;
+    .chart {
+      width: 100%;
+      height: 336rpx;
+    }
+  }
+
+  .no-data {
+    border-radius: 16rpx;
+    margin-bottom: 24rpx;
+    padding: 10rpx;
+    height: 336rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: #f9f9f9;
+    text {
+      color: #999;
+      font-size: 32rpx;
+    }
+  }
+
+  .stats-panel {
+    display: flex;
+    justify-content: space-between;
+    background-color: #f1f4f8;
+    border-radius: 16rpx;
+    padding: 24rpx 64rpx;
+    .stat-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      .label {
+        color: #656565;
+        font-size: 24rpx;
+        font-weight: 400;
+      }
+      .value {
+        font-size: 40rpx;
+        font-weight: bold;
+      }
+      .unit {
+        color: #999999;
+        font-size: 20rpx;
+      }
+      .red {
+        color: #fb4e52;
+      }
+    }
+  }
+}
+
+.history-section {
+  background-color: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx 32rpx;
+  .section-title {
+    font-size: 28rpx;
+    font-weight: bold;
+    margin-bottom: 16rpx;
+    color: #303133;
+  }
+  .table-wrap {
+    display: flex;
+    width: 100%;
+    box-sizing: border-box;
+    margin: 20rpx 0;
+    color: #042118;
+    font-family: "Source Han Sans CN VF";
+    font-size: 24rpx;
+    font-style: normal;
+    font-weight: 400;
+    line-height: normal;
+    .fixed-column {
+      width: 250rpx;
+      .table-cell {
+        width: 100%;
+        white-space: nowrap;
+      }
+    }
+    .table-bg {
+      width: 16rpx;
+      background: linear-gradient(270deg, #ffffff33 0%, #9598a433 100%);
+    }
+    .scroll-column {
+      flex: 1;
+      height: auto;
+      overflow: hidden;
+      margin-left: -10rpx;
+    }
+    .scroll-content {
+      min-width: 100%;
+      width: fit-content;
+      display: flex;
+      flex-direction: column;
+      .table-row {
+        display: flex;
+        .table-cell {
+          flex: 1;
+        }
+      }
+    }
+    .table-cell {
+      width: 160rpx;
+      height: 64rpx;
+      line-height: 64rpx;
+      text-align: center;
+      border-bottom: 2px solid #e4e7ed;
+      box-sizing: border-box;
+    }
+    .table-cell.header {
+      background: #f6f8fc;
+    }
+  }
+
+  .pagination {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-top: 24rpx;
+    .page-item {
+      width: 104rpx;
+      height: 48rpx;
+      color: #656565;
+      font-size: 24rpx;
+      line-height: 48rpx;
+      text-align: center;
+      border: 2rpx solid #e4e7ed;
+      border-radius: 8rpx;
+    }
+    .page-item.disabled {
+      color: #999;
+      border-color: #e4e7ed;
+      background-color: #f5f7fa;
+    }
+    .next {
+      color: #0bbc58;
+      border: 2rpx solid #0bbc58;
+    }
+    .next.disabled {
+      color: #999;
+      border-color: #e4e7ed;
+      background-color: #f5f7fa;
+    }
+    .page-info {
+      color: #999999;
+      font-family: "Source Han Sans CN VF";
+      font-size: 24rpx;
+      font-style: normal;
+      font-weight: 400;
+      line-height: normal;
+      .curret-page {
+        color: #303133;
+      }
+    }
+  }
+}
+::v-deep .u-calendar__action {
+  display: flex;
+  justify-content: center;
+}
+</style>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1456 - 0
pages/deviceDetails/SoilMoisturelist/index.vue


+ 345 - 0
pages/deviceDetails/SoilMoisturelist/simDetail.vue

@@ -0,0 +1,345 @@
+<template>
+  <view class="sim-details-page">
+    <cu-custom :isBack="true">
+      <template slot="content">
+        <view class="nav-title">SIM卡详情</view>
+      </template>
+    </cu-custom>
+    <!-- 加载动画 -->
+    <view v-if="loading" class="loading-container">
+      <u-loading mode="circle"></u-loading>
+      <text class="loading-text">加载中...</text>
+    </view>
+    <!-- 页面内容 -->
+    <view v-else>
+      <view class="sim-first sim-board">
+        <view class="title">数据SIM卡流量</view>
+        <view class="sim-item">
+          <text class="label">ICCID:</text>
+          <span class="val">{{ simData.iccid }}</span>
+          <image
+            @click="copy(simData.iccid)"
+            src="/static/images/device/copy.svg"
+            mode=""
+            class="info-img"
+          ></image>
+        </view>
+        <view class="sim-item">
+          <text class="label">总流量:</text>
+          <span class="val">{{ simData.total }}M</span>
+        </view>
+        <view class="sim-item">
+          <text class="label">状态:</text>
+          <text class="val">{{ simData.status }}</text>
+        </view>
+        <view class="sim-item">
+          <text class="label">到期时间:</text>
+          <text class="val">{{ simData.expire }}</text>
+        </view>
+        <view class="sim-item">
+          <text class="label">厂商名称:</text>
+          <text class="val">{{ simData.company }}</text>
+        </view>
+        <view class="sim-info-count">
+          <view class="sim-item">
+            <text class="label">已使用:</text
+            ><text
+              :style="{
+                color:
+                  simData.used > parseInt(simData.total) ? '#f56c6c' : '#0bbc58',
+              }"
+              >{{ simData.used.toFixed(2) }}M</text
+            >/{{ simData.total }}M
+          </view>
+          <view class="sim-item">
+            <text class="label">剩余:</text>
+            <text
+              class="val"
+              :style="{
+                color: '#0bbc58',
+              }"
+              >{{ (parseInt(simData.total || 0) - simData.used).toFixed(2) }}M</text
+            >
+          </view>
+        </view>
+      </view>
+      <view v-if="simIccidObj.photo_iccid || simIccidObj.is_editor" class="sim-board sim-second">
+        <view class="title">图片SIM卡流量</view>
+        <view class="sim-item">
+          <text class="label">ICCID:</text>
+          <span v-if="!isEditing" class="val">{{ simData.iccid }}</span>
+          <input
+            v-else
+            v-model="editingIccid"
+            type="text"
+            class="edit-input"
+            placeholder="请输入新的ICCID"
+          />
+          <image
+            @click="copy(isEditing ? editingIccid : simData.iccid)"
+            src="/static/images/device/copy.svg"
+            mode=""
+            class="info-img"
+          ></image>
+          <image
+            v-if="!isEditing"
+            @click="toggleEdit"
+            src="/static/images/device/edit.svg"
+            mode=""
+            class="info-img"
+          ></image>
+          <button v-else @click="saveEditIccid" class="save-btn">确定</button>
+        </view>
+      </view>
+      <view class="footer-board">
+        <u-button type="success" class="sub-btn" @click="goBack"
+          >确定</u-button
+        >
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  onLoad(options) {
+    this.loading = true
+    const deviceInfo = JSON.parse(decodeURIComponent(options.deviceInfo));
+    console.log(deviceInfo, "deviceInfo");
+    this.deviceInfo = deviceInfo;
+    this.getIccidByd_id();
+  },
+  data() {
+    return {
+      deviceInfo: {},
+      simData: {
+        iccid: "",
+        total: "",
+        status: "",
+        expire: "",
+        company: "",
+        used: 0,
+      },
+      isEditing: false,
+      editingIccid: "",
+      showSimData: false,
+      empty: false,
+      simIccidObj: {},
+      loading: false,
+    };
+  },
+  methods: {
+    getIccidByd_id() {
+      this.$myRequest({
+        url: "/api/api_gateway?method=forecast.send_control.device_sim_new",
+        data: {
+          d_id: this.deviceInfo.d_id,
+        },
+      }).then((res) => {
+        console.log('getIccidByd_id:', res);
+        if (
+          res.data_iccid ||
+          res.photo_iccid ||
+          res.is_editor
+        ) {
+          console.log(res, '1111')
+          this.empty = false;
+          this.simIccidObj = res;
+          if (res.data_iccid) {
+            this.active = "data_iccid";
+            console.log(222)
+            this.getSimData(res.data_iccid);
+          } else {
+            if (res.photo_iccid || res.is_editor) {
+              this.active = "photo_iccid";
+              if (res.photo_iccid) {
+                this.getSimData(res.photo_iccid);
+              } else {
+                this.loading = false;
+              }
+            }
+          }
+        } else {
+          this.empty = true;
+          this.loading = false;
+        }
+      });
+    },
+    getSimData(iccid) {
+      this.$myRequest({
+        url: "/api/api_gateway?method=forecast.send_control.sim_query_new",
+        data: {
+          iccid: iccid
+        }
+      }).then(res => {
+        console.log('simData:', res)
+        this.loading = false
+        this.simData = res
+        if (res.code) {
+          this.showSimData = false
+          // this.$message.error(res.data.data.msg)
+        } else {
+          this.showSimData = true
+          this.simData = res
+          this.editIccid = res.iccid
+        }
+      })
+    },
+    copy(item) {
+      console.log(item);
+      uni.setClipboardData({
+        data: item,
+        success: function () {
+          console.log("success");
+        },
+      });
+    },
+    toggleEdit() {
+      this.isEditing = !this.isEditing;
+      if (this.isEditing) {
+        this.editingIccid = this.simData.iccid;
+      }
+    },
+    saveIccid() {
+      this.simData.iccid = this.editingIccid;
+      this.isEditing = false;
+    },
+    saveEditIccid() {
+      this.$myRequest({
+        url: '/api/api_gateway?method=forecast.send_control.device_sim_update',
+        data: {
+          d_id: this.deviceInfo.d_id,
+          simid: this.editingIccid
+        }
+      }).then(res => {
+        if (res.code) {
+          this.isEditSim = false
+          this.loading = true
+          // this.$message.success(res.data.data.msg)
+          this.simIccidObj.photo_iccid = this.editingIccid
+          this.getSimData(this.editingIccid)
+        } else {
+          // this.$message.error(res.data.data.msg)
+        }
+        this.isEditing = false;
+      })
+    },
+    goBack() {
+      uni.navigateBack({
+        delta: 1,
+      })
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.sim-details-page {
+  background:
+    linear-gradient(180deg, #ffffff00 0%, #f5f6fa 23.64%, #f5f6fa 100%),
+    linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%);
+  min-height: 100vh;
+  padding: 0 32rpx;
+  padding-top: 98rpx;
+  box-sizing: border-box;
+}
+/* 加载动画样式 */
+.loading-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 60vh;
+  width: 100%;
+}
+.loading-text {
+  font-size: 28rpx;
+  color: #666;
+  margin-top: 32rpx;
+}
+.nav-title{
+  text-align: center;
+}
+.sim-board {
+  padding: 32rpx;
+  border-radius: 16rpx;
+  background: #fff;
+  margin-top: 46rpx;
+  margin-bottom: 24rpx;
+  .title {
+    color: #020305;
+    font-size: 32rpx;
+    font-weight: 700;
+    margin-bottom: 24rpx;
+  }
+  .sim-item {
+    font-size: 28rpx;
+    display: flex;
+    align-items: center;
+    margin-bottom: 24rpx;
+    .label {
+      color: #999999;
+    }
+    .val {
+      color: #303133;
+    }
+    .copy-icon,
+    .edit-icon {
+      width: 32rpx;
+      height: 32rpx;
+    }
+  }
+  .sim-info-count {
+    display: flex;
+    justify-content: space-between;
+  }
+}
+.footer-board {
+  border-top: 2rpx solid #e4e7ed;
+  background: #fff;
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 164rpx;
+  padding: 16rpx 32rpx 0 32rpx;
+  .sub-btn {
+    width: 100%;
+    background: #0bbc58;
+    border-radius: 16rpx;
+  }
+}
+.info-img {
+  width: 32rpx;
+  height: 32rpx;
+  margin-left: 16rpx;
+  background: #0bbc581a;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.edit-input {
+  flex: 1;
+  height: 48rpx;
+  border: 2rpx solid #0bbc58;
+  border-radius: 8rpx;
+  padding: 0 16rpx;
+  font-size: 28rpx;
+  color: #303133;
+  background-color: #f5f7fa;
+}
+.save-btn {
+  margin-left: 16rpx;
+  padding: 8rpx 20rpx;
+  background-color: #0bbc58;
+  color: #fff;
+  border: none;
+  border-radius: 8rpx;
+  font-size: 24rpx;
+  height: 48rpx;
+  line-height: 28rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+</style>

+ 121 - 0
pages/deviceDetails/weatherStation2/charts.vue

@@ -0,0 +1,121 @@
+<template>
+  <view>
+    <canvas
+      canvas-id="orIfOOkumJYtFcnuJkjxcySENqRtXceW"
+      id="orIfOOkumJYtFcnuJkjxcySENqRtXceW"
+      class="charts"
+      @touchstart="touchstart"
+      @touchmove="touchmove"
+      @touchend="touchend"
+    />
+  </view>
+</template>
+
+<script>
+import uCharts from "../../../components/js_sdk/u-charts/u-charts/u-charts.js";
+var uChartsInstance = {};
+export default {
+  data() {
+    return {
+      cWidth: 320,
+      cHeight: 200,
+    };
+  },
+  onReady() {
+    //这里的 750 对应 css .charts 的 width
+    // this.cWidth = uni.upx2px(340);
+    //这里的 500 对应 css .charts 的 height
+    // this.cHeight = uni.upx2px(200);
+    this.getServerData();
+  },
+  methods: {
+    getServerData() {
+      //模拟从服务器获取数据时的延时
+      setTimeout(() => {
+        //模拟服务器返回数据,如果数据格式和标准格式不同,需自行按下面的格式拼接
+        let res = {
+          categories: ["2018", "2019", "2020", "2021", "2022", "2023"],
+          series: [
+            {
+              name: "成交量A",
+              linearColor: [
+                [0, "#1890FF"],
+                [0.25, "#00B5FF"],
+                [0.5, "#00D1ED"],
+                [0.75, "#00E6BB"],
+                [1, "#90F489"],
+              ],
+              data: [35, 8, 25, 37, 4, 20],
+            },
+          ],
+        };
+        this.drawCharts("orIfOOkumJYtFcnuJkjxcySENqRtXceW", res);
+      }, 500);
+    },
+    drawCharts(id, data) {
+      const ctx = uni.createCanvasContext(id, this);
+      uChartsInstance[id] = new uCharts({
+        type: "line",
+        context: ctx,
+        width: this.cWidth,
+        height: this.cHeight,
+        categories: data.categories,
+        series: data.series,
+        animation: true,
+        background: "#FFFFFF",
+        color: [
+          "#1890FF",
+          "#91CB74",
+          "#FAC858",
+          "#EE6666",
+          "#73C0DE",
+          "#3CA272",
+          "#FC8452",
+          "#9A60B4",
+          "#ea7ccc",
+        ],
+        padding: [10, 10, 10, 10],
+        enableScroll: true,
+        legend: {
+          position: "top",
+        },
+        xAxis: {
+          disableGrid: true,
+          scrollShow: true,
+          itemCount: 4,
+        },
+        yAxis: {
+          gridType: "dash",
+          dashLength: 2,
+        },
+        dataLabel: false,
+        extra: {
+          line: {
+            type: "curve",
+            width: 2,
+            activeType: "hollow",
+          },
+        },
+      });
+    },
+    touchstart(e) {
+      uChartsInstance[e.target.id].scrollStart(e);
+    },
+    touchmove(e) {
+      uChartsInstance[e.target.id].scroll(e);
+    },
+    touchend(e) {
+      uChartsInstance[e.target.id].scrollEnd(e);
+      uChartsInstance[e.target.id].touchLegend(e);
+      uChartsInstance[e.target.id].showToolTip(e);
+    },
+  },
+};
+</script>
+
+<style scoped>
+.charts {
+  width: 750rpx;
+  height: 500rpx;
+}
+</style>

+ 181 - 0
pages/deviceDetails/weatherStation2/devControl.vue

@@ -0,0 +1,181 @@
+<template>
+  <view class="device-details-page">
+    <cu-custom :isBack="true">
+      <template slot="content">
+        <view class="nav-title">设备控制</view>
+      </template>
+    </cu-custom>
+    <view class="control-board">
+      <view class="title">操作</view>
+      <div style="margin-bottom: 48rpx;">
+        <u-button type="success" class="reload-btn" @click="handleControl('reboot')">重启</u-button>
+      </div>
+      <u-button type="success" class="reload-btn" @click="handleControl('update')">升级</u-button>
+      <u-button type="success" class="reload-btn" @click="handleControl('read')">查询上传时间间隔</u-button>
+      <view class="title date-title">上传时间间隔(min)</view>
+      <view class="date-slider">
+        <slider
+          v-model="interval"
+          show-value="true"
+          :min="1"
+          :max="240"
+          @change="changeDataInterval"
+          block-color="#57C878"
+          activeColor="#57C878"
+          step="1"
+        />
+      </view>
+       <u-button type="success" class="sub-btn" @click="handleControl2('interval')">确定</u-button>
+    </view>
+    <!-- <view class="footer-board">
+      <u-button type="success" class="sub-btn" @click="handleSubmit()">确定</u-button>
+    </view> -->
+  </view>
+</template>
+
+<script>
+export default {
+  onLoad(options) {
+    const deviceInfo = JSON.parse(decodeURIComponent(options.deviceInfo));
+    console.log(deviceInfo, "deviceInfo");
+    this.deviceInfo = deviceInfo;
+    this.getDevControlParams();
+  },
+  data() {
+    return {
+      deviceInfo: {},
+      interval: "",
+    };
+  },
+  methods: {
+    changeDataInterval(v){
+      this.interval = v.detail.value
+    },
+    // 获取设备控制参数信息
+    async getDevControlParams() {
+      await this.$myRequest({
+        url: '/api/api_gateway?method=greenhouse.env_first.control_device',
+        data: {
+          device_id: this.deviceInfo.devBid,
+        }
+      }).then(res => {
+        console.log('设备控制参数信息', res)
+        this.interval = res.interval
+      })
+    },
+    async handleControl(cmd) {
+      let params = {};
+      if(cmd == 'interval'){
+        params = {
+          device_id: this.deviceInfo.devBid,
+          interval: this.interval,
+        };
+      }else if(cmd == 'read'){
+        params = {
+          device_id: this.deviceInfo.devBid,
+          pub_code: JSON.stringify({cmd,ext:{type:'interval'}}),
+        };
+      }else{
+        params = {
+          device_id: this.deviceInfo.devBid,
+          pub_code: JSON.stringify({cmd}),
+        };
+      }
+      await this.$myRequest({
+        url: '/api/api_gateway?method=weather.weather.qxz_send_control',
+        data: params
+      }).then(res => {
+        console.log('设备控制参数信息', res)
+        uni.showToast({
+          title: "操作成功",
+          icon: "none",
+        });
+      })
+    },
+    async handleControl2(){
+      console.log(this.deviceInfo,'handleControl2handleControl2')
+      const params = {
+        device_type_id: 5,
+        d_id: this.deviceInfo.d_id,
+        config: JSON.stringify({content:'',timeout:'',interval:this.interval}),
+      }
+      await this.$myRequest({
+        url: '/api/api_gateway?method=forecast.send_control.device_control',
+        data: params
+      }).then(res => {
+        console.log('设备控制参数信息', res)
+        uni.showToast({
+          title: "操作成功",
+          icon: "none",
+        });
+      })
+    },
+    async handleSubmit() {
+      await this.handleControl('STORAGE_INR');
+      await this.handleControl('INTERVAL');
+      uni.showToast({
+        title: "操作成功",
+        icon: "none",
+      });
+    }
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.device-details-page {
+  background: linear-gradient(
+      180deg,
+      #ffffff00 0%,
+      #f5f6fa 23.64%,
+      #f5f6fa 100%
+    ),
+    linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%);
+  min-height: 100vh;
+  padding: 0 32rpx;
+  padding-top: 98rpx;
+}
+.nav-title{
+  text-align: center;
+}
+.control-board {
+  border-radius: 16rpx;
+  background: #fff;
+  padding: 32rpx;
+  margin-top: 46rpx;
+  .title {
+    margin-bottom: 16rpx;
+    color: #303133;
+    font-size: 28rpx;
+    font-weight: 400;
+  }
+  .date-title {
+    margin-bottom: 52rpx;
+  }
+  .reload-btn {
+    width: 100%;
+    background: #0bbc58;
+    margin-bottom: 48rpx;
+    border-radius: 16rpx;
+  }
+  .date-slider {
+    
+  }
+}
+
+.footer-board {
+  border-top: 2rpx solid #e4e7ed;
+  background: #fff;
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 164rpx;
+  padding: 16rpx 32rpx 0 32rpx;
+  .sub-btn {
+    width: 100%;
+    background: #0bbc58;
+    border-radius: 16rpx;
+  }
+}
+</style>

+ 350 - 0
pages/deviceDetails/weatherStation2/devRepair.vue

@@ -0,0 +1,350 @@
+<template>
+  <view class="device-repair-page">
+    <cu-custom :isBack="true">
+      <template slot="content">
+        <view class="nav-title">设备报修</view>
+      </template>
+    </cu-custom>
+    <view class="form-board">
+      <u-form
+        ref="baseFormRef"
+        :model="baseForm"
+        labelWidth="162"
+        :rules="rules"
+        errorType="toast"
+      >
+        <view class="repaid-board">
+          <view class="title">基本信息</view>
+          <u-form-item label="设备ID" prop="device_id" required>
+            <u-input
+              v-model="baseForm.device_id"
+              placeholder="设备ID"
+              disabled
+            ></u-input>
+          </u-form-item>
+          <u-form-item label="设备名称" prop="devName" required>
+            <u-input
+              v-model="baseForm.devName"
+              placeholder="请输入设备名称"
+              disabled
+            ></u-input>
+          </u-form-item>
+          <u-form-item label="联系人" prop="user" required>
+            <u-input
+              v-model="baseForm.user"
+              placeholder="请输入"
+            ></u-input>
+          </u-form-item>
+          <u-form-item label="联系人电话" prop="userphone" required>
+            <u-input
+              v-model="baseForm.userphone"
+              placeholder="请输入"
+              type="number"
+            ></u-input>
+          </u-form-item>
+          <u-form-item label="联系人地址" prop="addr" required>
+            <u-input
+              v-model="baseForm.addr"
+              placeholder="请输入"
+            ></u-input>
+          </u-form-item>
+        </view>
+        <view class="repaid-board">
+          <view class="title">报修详情</view>
+          <u-input
+            v-model="baseForm.errordesc"
+            placeholder="请描述设备问题"
+            type="textarea"
+            :height="216"
+            maxlength="140"
+            show-word-limit
+          ></u-input>
+
+          <view class="upload-section">
+            <view class="upload-title"
+              >设备照片<text class="info">(最多4张)</text></view
+            >
+            <u-upload
+              :file-list="fileList"
+              :max-count="4"
+              @on-choose-complete="onChooseComplete"
+              @on-remove="onRemove"
+              name="upload"
+              :max-size="5 * 1024 * 1024"
+              accept="image/*"
+              :auto-upload="false"
+              :show-progress="false"
+            >
+              <view slot="addBtn" class="add-btn">
+                <u-icon name="plus" size="56" color="#909399"></u-icon>
+              </view>
+            </u-upload>
+          </view>
+        </view>
+      </u-form>
+    </view>
+
+    <view class="footer-board">
+      <u-button type="success" class="sub-btn" @click="submitForm"
+        >确定</u-button
+      >
+    </view>
+  </view>
+</template>
+
+<script>
+import uInput from "../../../uview-ui/components/u-input/u-input.vue";
+export default {
+  components: { uInput },
+  onLoad(options) {
+    const deviceInfo = JSON.parse(decodeURIComponent(options.deviceInfo));
+    console.log("deviceInfo:", deviceInfo);
+    this.baseForm.device_id = deviceInfo.devBid
+    this.baseForm.devName = deviceInfo.devName
+  },
+  data() {
+    return {
+      baseForm: {
+        device_id: "",
+        devName: "",
+        user: "",
+        userphone: "",
+        addr: "",
+        errordesc: "",
+      },
+      rules: {
+        devName: [
+          {
+            required: true,
+            message: "请输入设备名称",
+            trigger: ["blur", "change"],
+          },
+        ],
+        user: [
+          {
+            required: true,
+            message: "请输入联系人",
+            trigger: ["blur", "change"],
+          },
+        ],
+        userphone: [
+          {
+            required: true,
+            message: "请输入联系人电话",
+            trigger: ["blur", "change"],
+          },
+          {
+            pattern: /^1[3-9]\d{9}$/,
+            message: "请输入正确的手机号",
+            trigger: ["blur", "change"],
+          },
+        ],
+        addr: [
+          {
+            required: true,
+            message: "请输入联系人地址",
+            trigger: ["blur", "change"],
+          },
+        ],
+        errordesc: [
+          {
+            required: true,
+            message: "请描述设备问题",
+            trigger: ["blur", "change"],
+          },
+        ],
+      },
+      fileList: [],
+    };
+  },
+  methods: {
+    onChooseComplete(lists) {
+      const newFiles = lists.filter(
+        (item) => !this.fileList.some((f) => f.url === item.url)
+      );
+      newFiles.forEach((file) => {
+        const item = {
+          url: file.url,
+          status: "uploading",
+        };
+        this.fileList.push(item);
+        const index = this.fileList.length - 1;
+        uni.uploadFile({
+          url: "https://wx.hnyfwlw.com/api/api_gateway?method=base.bases.base_photo",
+          filePath: file.url,
+          name: "img_file",
+          formData: {
+            user: "test",
+          },
+          success: (uploadFileRes) => {
+            const data = JSON.parse(uploadFileRes.data);
+            if (data.data && data.data.src) {
+              this.fileList[index].status = "success";
+              this.fileList[index].src = data.data.src;
+              this.$forceUpdate();
+              console.log("上传成功,图片地址:", data.data.src);
+            } else {
+              this.fileList[index].status = "failed";
+              uni.showToast({
+                title: data.msg || "上传失败",
+                icon: "none",
+              });
+            }
+          },
+          fail: (err) => {
+            this.fileList[index].status = "failed";
+            uni.showToast({
+              title: "上传失败",
+              icon: "none",
+            });
+            console.error("上传失败:", err);
+          },
+        });
+      });
+    },
+    onRemove(index) {
+      this.fileList.splice(index, 1);
+    },
+    submitForm() {
+      const hasUploading = this.fileList.some((f) => f.status === "uploading");
+      if (hasUploading) {
+        uni.showToast({
+          title: "请等待图片上传完成",
+          icon: "none",
+        });
+        return;
+      }
+      const hasFailed = this.fileList.some((f) => f.status === "failed");
+      if (hasFailed) {
+        uni.showToast({
+          title: "有图片上传失败,请重新上传",
+          icon: "none",
+        });
+        return;
+      }
+      this.$refs.baseFormRef
+        .validate()
+        .then((res) => {
+          const uploadedImages = this.fileList
+            .filter((f) => f.status === "success" && f.src)
+            .map((f) => f.src);
+          const params = {
+            ...this.baseForm,
+            errorimg: uploadedImages,
+          };
+          console.log("提交参数:", params);
+          // 
+          this.$myRequest({
+            url: "/api/api_gateway?method=after_sale.after_sale_manage.aftersale_apply",
+            data: params,
+          }).then(res => {
+            console.log("aftersale_apply:", res);
+            if(res){
+              uni.showToast({
+                  title: '提交成功',
+                  duration: 2000,
+              });
+              setTimeout(()=>{
+                uni.navigateBack({
+                  delta:1
+                })
+              },1000)
+            }else{
+              uni.showToast({
+                  title: '提交失败',
+                  duration: 2000,
+                icon:"none"
+              });
+            }
+          })
+        })
+        .catch((errors) => {
+          console.log("表单验证失败", errors);
+        });
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.device-repair-page {
+  background:
+    linear-gradient(180deg, #ffffff00 0%, #f5f6fa 23.64%, #f5f6fa 100%),
+    linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%);
+  min-height: 100vh;
+  padding: 0 32rpx;
+  padding-top: 98rpx;
+  padding-bottom: 200rpx;
+}
+
+.form-board {
+  margin-top: 46rpx;
+}
+.repaid-board {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 32rpx;
+  margin-bottom: 24rpx;
+
+  .title {
+    color: #020305;
+    font-size: 32rpx;
+    font-weight: bold;
+  }
+}
+
+.upload-section {
+  padding: 20rpx 0;
+
+  .upload-title {
+    font-size: 28rpx;
+    color: #303133;
+    margin-bottom: 16rpx;
+    .info {
+      color: #999;
+    }
+  }
+
+  .add-btn {
+    background: #f1f4f8;
+    width: 160rpx;
+    height: 160rpx;
+    border-radius: 12rpx;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+}
+
+// 使所有输入框文本右对齐
+::v-deep .uni-input-input,
+::v-deep .uni-input-placeholder {
+  text-align: right !important;
+}
+::v-deep .u-add-wrap {
+  display: none !important;
+}
+::v-deep .u-input__textarea {
+  background: #f1f4f8 !important;
+  border-radius: 8rpx;
+  margin-top: 24rpx;
+  padding: 17rpx 24rpx !important;
+}
+
+.footer-board {
+  border-top: 2rpx solid #e4e7ed;
+  background: #fff;
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 164rpx;
+  padding: 16rpx 32rpx 0 32rpx;
+
+  .sub-btn {
+    width: 100%;
+    background: #0bbc58;
+    border-radius: 16rpx;
+  }
+}
+</style>

+ 899 - 0
pages/deviceDetails/weatherStation2/eleDetail.vue

@@ -0,0 +1,899 @@
+<template>
+  <view class="ele-details-page">
+    <cu-custom :isBack="true">
+      <template slot="content">
+        <view class="nav-title">气象/墒情监测</view>
+      </template>
+    </cu-custom>
+    <view class="date-picker" @click="openDatePicker">
+      <text>{{ startDate }}</text>
+      <text class="separator">-</text>
+      <text>{{ endDate }}</text>
+      <image class="date-icon" src="/static/images/device/date-active.svg" />
+    </view>
+    <u-calendar
+      v-model="show"
+      :mode="mode"
+      @change="onDateChange"
+      ref="calendar" 
+      range-color="#999"
+      btn-type="success"
+      active-bg-color="#0BBC58"
+      range-bg-color="rgba(11,188,88,0.13)"
+    ></u-calendar>
+    <view class="echarts-board">
+      <view class="period-tabs">
+        <view
+          class="tab-item"
+          :class="{ active: activePeriod === '24h' }"
+          @click="changePeriod('24h')"
+          >24小时</view
+        >
+        <view
+          class="tab-item"
+          :class="{ active: activePeriod === '7d' }"
+          @click="changePeriod('7d')"
+          >一周</view
+        >
+        <view
+          class="tab-item"
+          :class="{ active: activePeriod === '30d' }"
+          @click="changePeriod('30d')"
+          >一月</view
+        >
+      </view>
+
+      <view class="element-tabs" v-if="hasData">
+        <view class="ele-board">
+          <view
+            v-for="(item, index) in eleTabsList"
+            :key="index"
+            class="ele-item"
+            :class="{ 'ele-active': eleActive === index }"
+            @click="changeElement(index,item)"
+            >{{ item.name }}</view
+          >
+        </view>
+        <view class="fold-icon" @click="toggleFold">
+          <u-icon :name="isFolded ? 'arrow-down' : 'arrow-up'"></u-icon>
+        </view>
+      </view>
+      <!-- 展开后的所有要素 -->
+      <view v-if="!isFolded && hasData" class="expanded-elements">
+        <view
+          class="expanded-item"
+          v-for="(item, index) in eleTabsList"
+          :key="index"
+          :class="{ 'ele-active': eleActive === index }"
+          @click="changeElement(index,item)"
+        >
+          {{ item.name }}
+        </view>
+      </view>
+
+      <view class="chart-container" v-if="hasData">
+        <canvas
+          v-show="!show"
+          canvas-id="temperatureChart"
+          id="temperatureChart"
+          class="chart"
+          @touchstart="touchLineA($event)"
+          @touchmove="moveLineA($event)"
+          @touchend="touchEndLineA($event)"
+          disable-scroll="true"
+        ></canvas>
+      </view>
+      <view class="no-data" v-else>
+        <text>暂无数据</text>
+      </view>
+
+      <!-- <view class="stats-panel">
+        <view class="stat-item">
+          <text class="value">27<text class="unit">°C</text></text>
+          <text class="label">最低值</text>
+        </view>
+        <view class="stat-item">
+          <text class="value">32<text class="unit">°C</text></text>
+          <text class="label">平均值</text>
+        </view>
+        <view class="stat-item">
+          <text class="value red">42<text class="unit red">°C</text></text>
+          <text class="label">最高值</text>
+        </view>
+      </view> -->
+    </view>
+
+    <view class="history-section">
+      <view class="section-title">历史数据</view>
+      <view class="table-wrap">
+        <view class="fixed-column">
+          <view class="table-cell header">上报时间</view>
+          <view class="table-cell" v-for="(item, idx) in tableList" :key="idx">
+            {{ formatTimestamp(item.time) }}
+          </view>
+        </view>
+        <view class="table-bg"></view>
+        <scroll-view class="scroll-column" scroll-x="true">
+          <view class="scroll-content">
+            <view class="table-row">
+              <view class="table-cell header">{{
+                eleKey + "(" + unit + ")"
+              }}</view>
+            </view>
+            <view class="table-row" v-for="(item, idx) in tableList" :key="idx">
+              <view class="table-cell">{{ item.value }}</view>
+            </view>
+          </view>
+        </scroll-view>
+      </view>
+
+      <view class="pagination">
+        <view
+          class="page-item prev"
+          @click="prevPage"
+          :class="{ disabled: currentPage === 1 }"
+          >上一页</view
+        >
+        <view class="page-info">
+          <text class="curret-page">{{ currentPage }}</text>
+          <text>/</text>
+          <text>{{ totalPages }}</text>
+        </view>
+        <view
+          class="page-item next"
+          @click="nextPage"
+          :class="{ disabled: currentPage >= totalPages }"
+          >下一页</view
+        >
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import uCharts from "../../../components/js_sdk/u-charts/u-charts/u-charts.js";
+import baseCharts from "./charts.vue";
+export default {
+  components: { baseCharts },
+  onLoad(options) {
+    const deviceInfo = JSON.parse(decodeURIComponent(options.deviceInfo));
+    this.eleKey = options.eleKey || "";
+    this.unit = options.unit || "";
+    this.key = options.key || "";
+    this.initDateRange(deviceInfo.uptime);
+    this.deviceInfo = deviceInfo;
+    this.getChartData();
+    this.getObData();
+  },
+  data() {
+    return {
+      unit: "",
+      key: "",
+      temperatureChart: null,
+      activePeriod: "",
+      activeElement: "temperature",
+      show: false,
+      mode: "range",
+      startDate: "",
+      endDate: "",
+      isFolded: true, // 控制要素是否收起
+      eleTabs: [],
+      eleTabsList: [],
+      eleActive: 0,
+      hasData: true,
+      tableList: [],
+      tableAllList: [],
+      titles: [],
+      currentPage: 1,
+      pageSize: 9,
+      total: 0,
+      chartData: {
+        categories: [],
+        series: [],
+      },
+      activeChartInfo: {},
+      eleKey: "",
+    };
+  },
+  computed: {
+    totalPages() {
+      return Math.ceil(this.total / this.pageSize);
+    },
+  },
+  methods: {
+    formatTimestamp(timestamp) {
+      if (!timestamp) return '';
+      const date = new Date(Number(timestamp) * 1000);
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, '0');
+      const day = String(date.getDate()).padStart(2, '0');
+      const hours = String(date.getHours()).padStart(2, '0');
+      const minutes = String(date.getMinutes()).padStart(2, '0');
+      const seconds = String(date.getSeconds()).padStart(2, '0');
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+    },
+    initDateRange(timestamp) {
+      if (!timestamp) {
+        const today = new Date();
+        this.endDate = this.formatDate(today);
+        const startDate = new Date(today);
+        startDate.setDate(startDate.getDate() - 7);
+        this.startDate = this.formatDate(startDate);
+        return;
+      }
+      const date = new Date(Number(timestamp) * 1000);
+      this.endDate = this.formatDate(date);
+      const startDate = new Date(date);
+      startDate.setDate(startDate.getDate() - 7);
+      this.startDate = this.formatDate(startDate);
+    },
+    formatDate(date) {
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, "0");
+      const day = String(date.getDate()).padStart(2, "0");
+      return `${year}-${month}-${day}`;
+    },
+    initChart() {
+      const ctx = uni.createCanvasContext("temperatureChart", this);
+      const currentEle = this.eleTabs.get(this.key);
+      // u-charts 的 series.data 必须是数字数组;原数据 value 为字符串(含 "/"、"NA"),
+      // 直接传对象数组会导致 Y 轴 min/max 计算异常,刻度全部相同
+      const seriesData = currentEle.map((item) => {
+        const val = item.value;
+        if (val === "/" || val === "NA" || val === null || val === undefined || val === "") {
+          return 0;
+        }
+        const num = Number(val);
+        return isNaN(num) ? 0 : num;
+      });
+      const series = [
+        {
+          name: currentEle[0].name,
+          data: seriesData,
+          lineWidth: 3,
+          pointShape: "circle",
+          pointSize: 12,
+          pointColor: currentEle.color,
+          pointBorderColor: "#fff",
+          pointBorderWidth: 2,
+          symbol: "emptyCircle",
+          symbolSize: 6,
+          legendShape: "circle",
+        },
+      ];
+      const xData = currentEle.map((item) => item.time);
+
+      this.temperatureChart = new uCharts({
+        $this: this,
+        canvasId: "temperatureChart",
+        type: "line",
+        context: ctx,
+        width: 340,
+        height: 180,
+        categories: xData,
+        series: series,
+        legend: {
+          position: "top",
+          show: true,
+          padding: 5,
+          fontColor: "#303133",
+          fontSize: 12,
+          itemType: "circle",
+          itemShape: "emptyCircle",
+          itemWidth: 12,
+          itemHeight: 12,
+          lineWidth: 2,
+        },
+        xAxis: {
+          axisLineColor: "#E4E7ED",
+          disableGrid: true,
+          fontColor: "#656565",
+          fontSize: 10,
+          itemCount: 5,
+          scrollShow: true,
+        },
+        yAxis: {
+          tofix: 2,
+          axisLineColor: "#E4E7ED",
+          fontColor: "#656565",
+          gridColor: "#E4E7ED",
+          dashLength: 4,
+          disableGrid: false,
+          fontSize: 10,
+        },
+        dataLabel: false,
+        enableScroll: true,
+        background: "#FFFFFF",
+        extra: {
+          line: {
+            type: "curve",
+            width: 2,
+            activeType: "hollow",
+          },
+        },
+      });
+    },
+    touchLineA(e) {
+      this.temperatureChart.scrollStart(e);
+    },
+    moveLineA(e) {
+      this.temperatureChart.scroll(e);
+    },
+    touchEndLineA(e) {
+      this.temperatureChart.scrollEnd(e);
+      //下面是toolTip事件,如果滚动后不需要显示,可不填写
+      // this.temperatureChart.showToolTip(e, {
+      //   format: function(item, category) {
+      //     console.log(item, 'iyee', e)
+      //     return category + ' ' + item.name + ':' + item.data
+      //   }
+      // });
+    },
+
+    // 打开日期选择器
+    openDatePicker() {
+      this.show = true;
+      // 延迟设置日期,确保组件已经初始化
+      this.$nextTick(() => {
+        if (this.$refs.calendar) {
+          // 直接设置组件内部的日期状态
+          const calendar = this.$refs.calendar;
+          calendar.startDate = this.startDate;
+          calendar.endDate = this.endDate;
+
+          // 如果有选中的日期范围,设置对应的年、月、日
+          if (this.startDate) {
+            const startParts = this.startDate.split("-");
+            calendar.startYear = parseInt(startParts[0]);
+            calendar.startMonth = parseInt(startParts[1]);
+            calendar.startDay = parseInt(startParts[2]);
+          }
+          if (this.endDate) {
+            const endParts = this.endDate.split("-");
+            calendar.endYear = parseInt(endParts[0]);
+            calendar.endMonth = parseInt(endParts[1]);
+            calendar.endDay = parseInt(endParts[2]);
+          }
+
+          // 如果有开始日期,设置日历当前显示的年月为开始日期的年月
+          if (this.startDate) {
+            const startParts = this.startDate.split("-");
+            calendar.year = parseInt(startParts[0]);
+            calendar.month = parseInt(startParts[1]);
+            calendar.changeData();
+          }
+        }
+      });
+    },
+
+    // 日期选择变化事件
+    onDateChange(e) {
+      this.startDate = e.startDate;
+      this.endDate = e.endDate;
+      this.activePeriod = "custom"; // 设置为自定义周期
+      this.show = false;
+      this.getChartData();
+      this.getObData();
+    },
+
+    // 切换时间周期
+    changePeriod(period) {
+      this.activePeriod = period;
+
+      // 根据选择的周期自动计算日期范围
+      const now = new Date();
+      const endDate = now.toISOString().split("T")[0];
+      const startDate = new Date();
+
+      if (period === "24h") {
+        startDate.setDate(now.getDate());
+      } else if (period === "7d") {
+        startDate.setDate(now.getDate() - 7);
+      } else if (period === "30d") {
+        startDate.setMonth(now.getMonth() - 1);
+      }
+
+      this.startDate = startDate.toISOString().split("T")[0];
+      this.endDate = endDate;
+      this.getChartData();
+      this.getObData();
+    },
+
+    // 切换要素标签
+    changeElement(index,item) {
+      if (this.eleTabs.length === 0) return;
+      this.eleActive = index;
+      this.activeChartInfo = this.eleTabs[index];
+      this.key = item.key
+      this.isFolded = true; // 关闭展开面板
+      const newData = [];
+      this.eleKey = item.name
+      this.unit = item.unit
+      this.tableAllList.forEach(item => {
+        const dat = item?.dat || {};
+        for(let key in dat){
+          if(key == this.key){
+            const value = dat[key] ? dat[key]?.split('#')?.[0] : 'N/A';
+            newData.push({
+              time: item.time,
+              value,
+            })
+          }
+        }
+      })
+      this.tableList = newData;
+      this.$nextTick(() => {
+        this.initChart();
+      });
+    },
+    getKeyByName(key){
+      const titles = [...this.titles]
+      let k
+      titles.forEach(title => {
+        if(title.name === key){
+          k = title.key
+        }
+      })
+      return k
+    },
+    // 切换要素展开/收起状态
+    toggleFold() {
+      this.isFolded = !this.isFolded;
+    },
+    // 上一页
+    prevPage() {
+      if (this.currentPage > 1) {
+        this.currentPage--;
+        this.getObData();
+      }
+    },
+    // 下一页
+    nextPage() {
+      if (this.currentPage < this.totalPages) {
+        this.currentPage++;
+        this.getObData();
+      }
+    },
+    // 折线图数据
+    getChartData() {
+      this.$myRequest({
+        url: "/api/api_gateway?method=weather.weather.qxz_data_chart",
+        data: {
+          device_id: this.deviceInfo.devBid,
+          begin: String(
+            Math.floor(new Date(this.startDate + " 00:00:00").getTime() / 1000),
+          ),
+          end: String(
+            Math.floor(new Date(this.endDate + " 23:59:59").getTime() / 1000),
+          ),
+        },
+      }).then((res) => {
+        console.log("getChartData:", res);
+        const resList = res?.data || []
+        const conf = res?.conf || {}
+        if (resList && Array.isArray(resList) && resList.length > 0) {
+          const map = new Map()
+          resList.forEach(item => {
+            const dat = item?.dat[1] || {};
+            for(let key in dat){
+              if(dat[key]){
+              const name = conf[key].split('#')[0]
+                if(map.has(key)){
+                  map.get(key).push({
+                    time: this.formatTime(item.time*1000,'yyyy-MM-dd'),
+                    value: dat[key],
+                    name,
+                    key
+                  })
+                }else{
+                  map.set(key, [{
+                    time: this.formatTime(item.time*1000,'yyyy-MM-dd'),
+                    value: dat[key],
+                    name,
+                    key
+                    }]
+                  )
+                }
+              }
+            }
+          })
+          this.eleTabs = map;
+          this.eleTabsList = [];
+          map.forEach((value, key) => {
+            this.eleTabsList.push({
+              name: conf[key].split('#')[0],
+              key,
+              unit: conf[key].split('#')[1],
+              value: value,
+            })
+          })
+          this.hasData = true;
+          this.$nextTick(() => {
+            this.initChart();
+          });
+        } else {
+          this.eleTabs = [];
+          this.hasData = false;
+        }
+      });
+    },
+    // 历史数据
+    getObData() {
+      this.$myRequest({
+        url: "/api/api_gateway?method=weather.weather.qxz_detail",
+        data: {
+          device_id: this.deviceInfo.devBid,
+          start_time: String(
+            Math.floor(new Date(this.startDate + " 00:00:00").getTime() / 1000),
+          ),
+          end_time: String(
+            Math.floor(new Date(this.endDate + " 23:59:59").getTime() / 1000),
+          ),
+          page: this.currentPage,
+          page_size: this.pageSize,
+        },
+      }).then((res) => {
+        console.log("getObData:", res);
+        const data = res.data || [];
+        const newData = [];
+        this.tableAllList = data;
+        data.forEach(item => {
+          const dat = item?.dat || {};
+          for(let key in dat){
+            if(key == this.key){
+              newData.push({
+                time: item.time,
+                value: dat[key].split('#')[0],
+              })
+            }
+          }
+        })
+        this.titles = res?.title || [];
+        this.tableList = newData;
+        this.total = res.nums;
+      });
+    },
+    convertDynamic(data, getLabelValue) {
+      if (!data || typeof data !== "object") return [];
+      const result = [];
+      const keys = Object.keys(data);
+      for (const prop of keys) {
+        const arr = data[prop];
+        const value = getLabelValue(prop, arr, data);
+        if (value !== undefined && value !== null) {
+          result.push({
+            label: String(value),
+            prop,
+            align: "center",
+          });
+        }
+      }
+      return result;
+    },
+    processData(data) {
+      return Object.entries(data).reduce((acc, [key, value]) => {
+        acc[key] = Array.isArray(value) ? value[0] : value;
+        return acc;
+      }, {});
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.ele-details-page {
+  background:
+    linear-gradient(180deg, #ffffff00 0%, #f5f6fa 23.64%, #f5f6fa 100%),
+    linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%);
+  min-height: 100vh;
+  padding: 0 32rpx;
+  padding-top: 98rpx;
+}
+.nav-title{
+  text-align: center;
+}
+.date-picker {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background-color: #fff;
+  border-radius: 40rpx;
+  padding: 16rpx 24rpx;
+  margin-bottom: 36rpx;
+  font-size: 28rpx;
+  color: #999;
+  margin-top: 46rpx;
+  .date-icon {
+    width: 32rpx;
+    height: 32rpx;
+  }
+}
+
+.separator {
+  margin: 0 16rpx;
+}
+.echarts-board {
+  padding: 32rpx;
+  border-radius: 16rpx;
+  background: #fff;
+  margin-bottom: 24rpx;
+  position: relative;
+  .period-tabs {
+    width: 350rpx;
+    height: 64rpx;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin: auto;
+    margin-bottom: 12rpx;
+    border-radius: 32rpx;
+    background: #f1f4f8;
+    padding: 8rpx 10rpx;
+    color: #303133;
+    font-family: "Source Han Sans CN VF";
+    font-size: 26rpx;
+    font-style: normal;
+    font-weight: 400;
+    line-height: normal;
+    .tab-item {
+      padding: 5rpx 24rpx;
+      height: 48rpx;
+      line-height: 48rpx;
+      border-radius: 32rpx;
+    }
+    .active {
+      background: #fff;
+    }
+  }
+  .element-tabs {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    overflow-x: auto;
+    white-space: nowrap;
+    .ele-board {
+      flex: 1;
+      display: flex;
+      align-items: center;
+      overflow-x: auto;
+      overflow-y: hidden;
+      white-space: nowrap;
+    }
+    .ele-item {
+      position: relative;
+      color: #999999;
+      font-size: 28rpx;
+      font-weight: 400;
+      white-space: nowrap;
+      margin-right: 16rpx;
+      padding: 14rpx 16rpx;
+      cursor: pointer;
+    }
+    .ele-active {
+      color: #303133;
+      font-weight: 500;
+      &::after {
+        content: "";
+        position: absolute;
+        left: 50%;
+        transform: translateX(-50%);
+        bottom: 4rpx;
+        display: block;
+        width: 30rpx;
+        height: 8rpx;
+        border-radius: 4rpx;
+        background: #1fc676;
+      }
+    }
+    .fold-icon {
+      width: 48rpx;
+      border-left: 2px solid #999;
+      padding-left: 16rpx;
+      margin-left: 16rpx;
+      cursor: pointer;
+      display: none;
+    }
+  }
+  /* 展开后的要素容器样式 */
+  .expanded-elements {
+    position: absolute;
+    top: 126rpx;
+    left: 0;
+    right: 0;
+    background: #f5f7fa;
+    border-radius: 12rpx;
+    padding: 16rpx;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 16rpx;
+    z-index: 10;
+    box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
+  }
+
+  .expanded-item {
+    flex: 0 0 calc(33.33% - 16rpx);
+    text-align: center;
+    padding: 16rpx 0;
+    border-radius: 8rpx;
+    color: #999999;
+    font-size: 26rpx;
+    background: #ffffff;
+    cursor: pointer;
+    transition: all 0.3s ease;
+  }
+
+  .expanded-item.ele-active {
+    color: #1fc676;
+    background: #e6f7ef;
+    font-weight: 500;
+  }
+
+  .chart-container {
+    border-radius: 16rpx;
+    margin-bottom: 24rpx;
+    position: relative;
+    padding: 10rpx;
+    overflow: hidden;
+    height: 336rpx;
+    z-index:0;
+    .chart {
+      width: 100%;
+      height: 336rpx;
+    }
+  }
+
+  .no-data {
+    border-radius: 16rpx;
+    margin-bottom: 24rpx;
+    padding: 10rpx;
+    height: 336rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: #f9f9f9;
+    text {
+      color: #999;
+      font-size: 32rpx;
+    }
+  }
+
+  .stats-panel {
+    display: flex;
+    justify-content: space-between;
+    background-color: #f1f4f8;
+    border-radius: 16rpx;
+    padding: 24rpx 64rpx;
+    .stat-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      .label {
+        color: #656565;
+        font-size: 24rpx;
+        font-weight: 400;
+      }
+      .value {
+        font-size: 40rpx;
+        font-weight: bold;
+      }
+      .unit {
+        color: #999999;
+        font-size: 20rpx;
+      }
+      .red {
+        color: #fb4e52;
+      }
+    }
+  }
+}
+
+.history-section {
+  background-color: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx 32rpx;
+  .section-title {
+    font-size: 28rpx;
+    font-weight: bold;
+    margin-bottom: 16rpx;
+    color: #303133;
+  }
+  .table-wrap {
+    display: flex;
+    width: 100%;
+    box-sizing: border-box;
+    margin: 20rpx 0;
+    color: #042118;
+    font-family: "Source Han Sans CN VF";
+    font-size: 24rpx;
+    font-style: normal;
+    font-weight: 400;
+    line-height: normal;
+    .fixed-column {
+      width: 250rpx;
+      .table-cell {
+        width: 100%;
+        white-space: nowrap;
+      }
+    }
+    .table-bg {
+      width: 16rpx;
+      background: linear-gradient(270deg, #ffffff33 0%, #9598a433 100%);
+    }
+    .scroll-column {
+      flex: 1;
+      height: auto;
+      overflow: hidden;
+      margin-left: -10rpx;
+    }
+    .scroll-content {
+      min-width: 100%;
+      width: fit-content;
+      display: flex;
+      flex-direction: column;
+      .table-row {
+        display: flex;
+        .table-cell {
+          flex: 1;
+        }
+      }
+    }
+    .table-cell {
+      width: 160rpx;
+      height: 64rpx;
+      line-height: 64rpx;
+      text-align: center;
+      border-bottom: 2px solid #e4e7ed;
+      box-sizing: border-box;
+    }
+    .table-cell.header {
+      background: #f6f8fc;
+    }
+  }
+
+  .pagination {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-top: 24rpx;
+    .page-item {
+      width: 104rpx;
+      height: 48rpx;
+      color: #656565;
+      font-size: 24rpx;
+      line-height: 48rpx;
+      text-align: center;
+      border: 2rpx solid #e4e7ed;
+      border-radius: 8rpx;
+    }
+    .page-item.disabled {
+      color: #999;
+      border-color: #e4e7ed;
+      background-color: #f5f7fa;
+    }
+    .next {
+      color: #0bbc58;
+      border: 2rpx solid #0bbc58;
+    }
+    .next.disabled {
+      color: #999;
+      border-color: #e4e7ed;
+      background-color: #f5f7fa;
+    }
+    .page-info {
+      color: #999999;
+      font-family: "Source Han Sans CN VF";
+      font-size: 24rpx;
+      font-style: normal;
+      font-weight: 400;
+      line-height: normal;
+      .curret-page {
+        color: #303133;
+      }
+    }
+  }
+}
+::v-deep .u-calendar__action {
+  display: flex;
+  justify-content: center;
+}
+</style>

+ 996 - 0
pages/deviceDetails/weatherStation2/index.vue

@@ -0,0 +1,996 @@
+<template>
+  <view class="device-details-page">
+    <cu-custom :isBack="true">
+      <template slot="content">
+        <view class="nav-title">{{ deviceInfo.devName }}</view>
+      </template>
+    </cu-custom>
+
+    <!-- 设备基本信息 -->
+    <view class="device-info">
+      <view class="operation" @click.stop="isShowOperation = !isShowOperation">操作</view>
+      <view class="operation-container" v-if="isShowOperation" @click="closeOperationHandler">
+      <view class="operation-background"></view>
+      <view class="operation-content">
+        <view class="operation-item" @click="goPage('devRepair')">
+          <image :src="serviceIcon" class="operation-icon"></image>
+          维修
+        </view>
+        <view class="operation-item" @click="goPage('devControl')">
+          <image :src="settingIcon" class="operation-icon"></image>
+          设置
+        </view>
+        <view class="operation-item" @click="goPage('simDetail')">
+          <image :src="simIcon" class="operation-icon"></image>
+          SIM卡
+        </view>
+        <!-- <view class="operation-item" @click="modification">
+          <image :src="editIcon" class="operation-icon"></image>
+          修改
+        </view> -->
+      </view>
+    </view>
+      <view @click="copy(deviceInfo.devBid)" class="info-item">
+        <text class="info-label">设备ID</text>
+        <text class="info-value">{{ deviceInfo.devBid }}</text>
+        <image
+          src="/static/images/device/copy.svg"
+          mode=""
+          class="info-img"
+        ></image>
+      </view>
+      <view class="info-item">
+        <text class="info-label">上报时间</text>
+        <text class="info-value">{{ formatTimestamp(deviceInfo.uptime) }}</text>
+      </view> 
+      <view class="info-item">
+        <text class="info-label">设备位置</text>
+        <text class="info-value">{{ deviceInfo.address }}</text>
+      </view>
+      <view class="dev-status">{{
+        deviceInfo.devStatus == '1' ? '在线' : '离线'
+      }}</view>
+    </view>
+
+    <!-- 标签页切换 -->
+    <view class="tab-container">
+      <view class="tab-top">
+        <view
+          class="tab-item"
+          :class="{ active: activeTab === 'monitoring' }"
+          @click="switchTab('monitoring')"
+        >
+          <text class="tab-text">监测要素</text>
+          <image
+            v-if="activeTab === 'monitoring'"
+            src="/static/images/device/smile.svg"
+            mode=""
+            class="smile"
+          ></image>
+        </view>
+        <view
+          class="tab-item"
+          :class="{ active: activeTab === 'details' }"
+          @click="switchTab('details')"
+        >
+          <text class="tab-text">设备数据</text>
+          <image
+            v-if="activeTab === 'details'"
+            src="/static/images/device/smile.svg"
+            mode=""
+            class="smile"
+          ></image>
+        </view>
+      </view>
+    </view>
+
+    <!-- 标签页内容 -->
+    <view class="tab-content">
+      <!-- 监测要素页面 -->
+      <view v-if="activeTab === 'monitoring'" class="monitoring-page">
+        <view
+          class="data-card"
+          v-for="(item, index) in monitorList"
+          :key="index"
+          @click="goPage('eleDetail', item)"
+        >
+          <view class="card-title">{{ item.name }}</view>
+          <view class="data-value-row">
+            <view>
+              <text v-if="item.value && item.value.length" class="data-value">{{
+                item.value
+              }}</text>
+              <text class="data-unit">{{ item.unit }}</text>
+            </view>
+            <image
+              class="device-img"
+              :src="`https://s3.hnyfwlw.com/webstaticimg/bigdata_app/icons/icon_${item.unitKey}.png`"
+              mode="aspectFit"
+            ></image>
+          </view>
+        </view>
+      </view>
+
+      <!-- 设备详情页面 -->
+      <view v-if="activeTab === 'details'" class="details-page">
+      <view class="device-params">
+          <view class="device-control">
+            <view class="control-left">
+              <view>{{formatTimestamp(deviceInfo.uptime)}}</view>
+              <u-icon name="reload" color="#0BBC58" style="margin-left: 12rpx" @click="refreshData"></u-icon>
+            </view>
+          </view>
+          <view class="params-board">
+            <view class="params-left-container">
+              <view class="params-left">
+                <view
+                  v-for="(item, index) in statusList.slice(0, 4)"
+                  :key="index"
+                  class="params-item"
+                >
+                  <view class="params-val">
+                    {{ item.value }}
+                    <span class="unit">{{ item.unit }}</span>
+                  </view>
+                  <view class="params-label">{{ item.name }}</view>
+                </view>
+              </view><view class="params-left">
+                <view
+                  v-for="(item, index) in statusList.slice(4, 8)"
+                  :key="index"
+                  class="params-item"
+                >
+                  <view class="params-val">
+                    {{ item.value }}
+                    <span class="unit">{{ item.unit }}</span>
+                  </view>
+                  <view class="params-label">{{ item.name }}</view>
+                </view>
+              </view>
+            </view>
+            <view class="params-right">
+              <image
+                class="device-img"
+                style="width: 150rpx; height: 300rpx;"
+                src="/static/images/device/qxz_low.png"
+                mode="aspectFit"
+              ></image>
+            </view>
+          </view>
+        </view>
+        <!-- 设备数据表格 -->
+        <view class="table-container">
+          <view class="table-control">
+            <view class="title">设备数据</view>
+            <view class="date-board" @click="openDatePicker">
+              <text>{{ startDate }}</text>
+              <text class="line">至</text>
+              <text>{{ endDate }}</text>
+              <image class="date-icon" src="/static/images/device/date.svg" />
+            </view>
+          </view>
+          <u-calendar
+            v-model="show"
+            :mode="mode"
+            @change="onDateChange"
+            range-color="#999" btn-type="success" active-bg-color="#0BBC58" range-bg-color="rgba(11,188,88,0.13)"
+            ref="calendar"
+          ></u-calendar>
+          <view class="table-wrap" v-if="tableData && tableData.length > 0">
+            <view class="fixed-column">
+              <view class="table-cell header">上报时间</view>
+              <view
+                class="table-cell"
+                v-for="(item, idx) in tableData"
+                :key="idx"
+              >
+                {{ formatTimestamp(item.time) }}
+              </view>
+            </view>
+            <view class="table-bg"></view>
+            <scroll-view class="scroll-column" scroll-x="true">
+              <view class="scroll-content">
+                <view class="table-row">
+                  <view
+                    class="table-cell header"
+                    v-for="(item, index) in tableColumn"
+                    :key="index"
+                    >{{ item.label }}</view
+                  >
+                </view>
+                <view
+                  class="table-row"
+                  v-for="(item, idx) in tableData"
+                  :key="idx"
+                >
+                  <view
+                    class="table-cell"
+                    v-for="(item1, index1) in tableColumn"
+                    :key="index1"
+                    >{{ formartValue(item.dat[item1.prop]) }}</view
+                  >
+                </view>
+              </view>
+            </scroll-view>
+          </view>
+          <view class="empty-state" v-else>
+            <u-empty text="暂无数据" mode="list"></u-empty>
+          </view>
+
+          <view class="pagination" v-if="tableData && tableData.length > 0">
+            <view
+              class="page-item prev"
+              :class="{ disabled: page <= 1 }"
+              @click="prevPage"
+              >上一页</view
+            >
+            <view class="page-info">
+              <text class="curret-page">{{ page }}</text>
+              <text>/</text>
+              <text>{{ totalPages }}</text>
+            </view>
+            <view
+              class="page-item next"
+              :class="{ disabled: page >= totalPages }"
+              @click="nextPage"
+              >下一页</view
+            >
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  onLoad(options) {
+    this.deviceInfo = options;
+    this.initDateRange(options.uptime);
+    this.getQxzObData();
+    // this.getDeviceStatus();
+    this.getDeviceData();
+  },
+  data() {
+    return {
+      deviceInfo: {},
+      isShowOperation: false,
+      activeTab: 'monitoring',
+      monitorList: [],
+      statusList: [],
+      show: false,
+      mode: 'range',
+      startDate: '',
+      endDate: '',
+      page: 1,
+      photoIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/photoIcon.png',
+      editIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/editIcon.png',
+      serviceIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/serviceIcon.png',
+      simIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/simIcon.png',
+      settingIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/settingIcon.png',
+      isShowPhoto:false,
+      pageSize: 9,
+      total: 0,
+      tableData: [],
+      tableColumn: [],
+    };
+  },
+  computed: {
+    totalPages() {
+      return Math.ceil(this.total / this.pageSize) || 1;
+    },
+  },
+  methods: {
+    formartValue(value){
+      return value?.split('#')?.[0] || '';
+    },
+    closeOperationHandler(){
+      this.isShowOperation = false;
+    },
+    async refreshData(){
+      //api/api_gateway?method=qxz.device.control
+      await this.$myRequest({
+        url: '/api/api_gateway?method=qxz.device.control',
+        data: {
+          device_id: this.deviceInfo.devBid,
+          cmd: 'READ',
+        },
+      });
+      //弹框提示
+      uni.showToast({
+        title: '刷新成功',
+        icon: 'success',
+      });
+      this.getDeviceData();
+    },
+    formatTimestamp(timestamp) {
+      if (!timestamp) return '';
+      const date = new Date(Number(timestamp) * 1000);
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, '0');
+      const day = String(date.getDate()).padStart(2, '0');
+      const hours = String(date.getHours()).padStart(2, '0');
+      const minutes = String(date.getMinutes()).padStart(2, '0');
+      const seconds = String(date.getSeconds()).padStart(2, '0');
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+    },
+    formatDate(date) {
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, '0');
+      const day = String(date.getDate()).padStart(2, '0');
+      return `${year}-${month}-${day}`;
+    },
+    initDateRange(timestamp) {
+      if (!timestamp) {
+        const today = new Date();
+        this.endDate = this.formatDate(today);
+        const startDate = new Date(today);
+        startDate.setDate(startDate.getDate() - 7);
+        this.startDate = this.formatDate(startDate);
+        return;
+      }
+      const date = new Date(Number(timestamp) * 1000);
+      this.endDate = this.formatDate(date);
+      const startDate = new Date(date);
+      startDate.setDate(startDate.getDate() - 7);
+      this.startDate = this.formatDate(startDate);
+    },
+    switchTab(tab) {
+      this.activeTab = tab;
+    },
+    copy(item) {
+      uni.setClipboardData({
+        data: item,
+        success: function () {
+        },
+      });
+    },
+    goPage(type, eleItem) {
+      let url = '';
+      console.log(type,eleItem,'deviceInfodeviceInfo')
+      if (eleItem && eleItem.key) {
+        url = `/pages/deviceDetails/weatherStation2/${type}?deviceInfo=${encodeURIComponent(JSON.stringify(this.deviceInfo))}&eleKey=${eleItem.name}&unit=${eleItem.unit}&key=${eleItem.key}`;
+      } else {
+        url = `/pages/deviceDetails/weatherStation2/${type}?deviceInfo=${encodeURIComponent(JSON.stringify(this.deviceInfo))}`;
+      }
+
+      if (type === 'devRepair') {
+        url = `/pages/afterSale/addafter?d_id=${this.deviceInfo.d_id}&device_id=${this.deviceInfo.devBid}&device_type=${this.deviceInfo.deviceType}`;
+      }
+
+      uni.navigateTo({
+        url,
+      });
+    },
+    openDatePicker() {
+      this.show = true;
+    },
+    onDateChange(event) {
+      if (event && event.startDate && event.endDate) {
+        this.startDate = event.startDate;
+        this.endDate = event.endDate;
+        this.show = false;
+        this.page = 1;
+        this.getDeviceData();
+      }
+    },
+    prevPage() {
+      if (this.page <= 1) return;
+      this.page--;
+      this.getDeviceData();
+    },
+    nextPage() {
+      if (this.page >= this.totalPages) return;
+      this.page++;
+      this.getDeviceData();
+    },
+    // 获取设备最新监测数据
+    async getQxzObData() {
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=weather.weather.qxz_status',
+        data: {
+          device_id: this.deviceInfo.devBid,
+        },
+      });
+      const recent =  [];
+      const conf = res.conf || {};
+      const data = res.dat || {};
+      for(let key in conf){
+        if(conf[key]){
+          const valueSplit = data[key]?.split('#') || [];
+          const value = valueSplit[0] == '-99.99' ? 'N/A' : valueSplit[0]
+          const unitKey = valueSplit[1]
+          const nameSplit = conf[key]?.split('#') || [];
+          const name = nameSplit[0]
+          const unit = nameSplit[1]
+          recent.push({
+            key,
+            unitKey,
+            name,
+            unit,
+            value,
+          })
+        }
+      }
+      this.statusList = recent;
+       this.monitorList = recent;
+    },
+    // 获取设备状态数据
+    async getDeviceStatus() {
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=qxz.data.qxz_status',
+        data: {
+          device_id: this.deviceInfo.devBid,
+        },
+      });
+      const newArr = this.transformObjectToArray(res);
+      this.statusList = newArr;
+    },
+    transformObjectToArray(originalData) {
+      if (
+        typeof originalData !== 'object' ||
+        originalData === null ||
+        Array.isArray(originalData)
+      ) {
+        return [];
+      }
+
+      return Object.keys(originalData).map((key) => {
+        const [val = '', label = ''] = originalData[key] || [];
+        return {
+          label,
+          val,
+          key,
+        };
+      });
+    },
+    // 设备数据列表
+    async getDeviceData() {
+      const data = await this.$myRequest({
+        url: '/api/api_gateway?method=weather.weather.qxz_detail',
+        data: {
+          device_id: this.deviceInfo.devBid,
+          page: String(this.page),
+          page_size: String(this.pageSize),
+          start_time: String(
+            Math.floor(new Date(this.startDate + ' 00:00:00').getTime() / 1000),
+          ),
+          end_time: String(
+            Math.floor(new Date(this.endDate + ' 23:59:59').getTime() / 1000),
+          ),
+        },
+      });
+      console.log(data,'datadatadata')
+      this.total = data?.nums || 0;
+      const table = [];
+      const conf = data.conf || [];
+      const dataList = data.data || [];
+      const tableColumn = [];
+      for(let key in conf){
+        if(conf[key]){
+          tableColumn.push({
+            label: conf[key]?.split('#')?.[0] || '',
+            prop: key,
+          })
+        }
+      }
+      // for(let i = 0;i< dataList.length;i++){
+      //   const item = dataList[i]?.dat || {};
+      //   for(let key in item){
+      //     if(item[key]){
+      //       const dataSplit = item[key].split('#');
+      //       table.push({
+      //         key,
+      //         unit: dataSplit[1],
+      //         value: dataSplit[0],
+      //         label: conf[key].split('#')[0],
+      //       })
+      //     }
+      //   }
+      // }
+      this.tableData = dataList || [];
+      console.log(this.tableData,'thistableData')
+      this.tableColumn = tableColumn;
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.device-details-page {
+  background:
+    linear-gradient(180deg, #ffffff00 0%, #f5f6fa 23.64%, #f5f6fa 100%),
+    linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%);
+  min-height: 100vh;
+  padding: 0 32rpx;
+  padding-top: 98rpx;
+}
+.nav-title{
+  //超出隐藏
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  text-align: center;
+}
+/* 设备基本信息 */
+.device-info {
+  background-color: #ffffff;
+  margin: 46rpx 0 24rpx 0;
+  padding: 32rpx 24rpx;
+  border-radius: 16rpx;
+  background: linear-gradient(180deg, #eff 0%, #fff 23.56%);
+  position: relative;
+  .info-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 16rpx;
+  }
+  .info-item:last-child {
+    margin-bottom: 0;
+  }
+
+
+.operation-container{
+  position: fixed;
+  right: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 99;
+}
+.operation{
+  position: fixed;
+  top: 260rpx;
+  right: 0;
+  z-index: 999;
+  width:48rpx;
+  height: 100rpx;
+  line-height: 50rpx;
+  border-radius: 8px 0 0 8px;
+  border-top: 4rpx solid #FFF;
+  border-bottom: 4rpx solid #FFF;
+  border-left: 4rpx solid #FFF;
+  background: #dddfe6a3;
+  color: #515153;
+  text-align: center;
+  font-family: "Source Han Sans CN VF";
+  font-size: 24rpx;
+  font-weight: 500;
+  writing-mode: vertical-rl;
+}
+.operation-background{
+  position: fixed;
+  right: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 998;
+  background: #00000040;
+}
+.operation-content{
+  position: fixed;
+  top: 240rpx;
+  right: 80rpx;
+  height: 126rpx;
+  display: flex;
+  padding: 16rpx 32rpx;
+  align-items: center;
+  gap: 64rpx;
+  z-index: 999;
+  border-radius: 32rpx;
+  border: 2rpx solid #FFF;
+  background: #FFF;
+  backdrop-filter: blur(8rpx);
+  .operation-item{
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    color: #333333;
+    text-align: center;
+    font-family: "Source Han Sans CN VF";
+    font-size: 24rpx;
+    font-weight: 400;
+  }
+  .operation-icon{
+    width: 58rpx;
+    height: 58rpx;
+  }
+}
+  .info-label {
+    width: 116rpx;
+    color: #999999;
+    text-align: right;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 28rpx;
+    font-style: normal;
+    font-weight: 400;
+    line-height: normal;
+    margin-right: 32rpx;
+    white-space: nowrap;
+  }
+
+  .info-value {
+    color: #333333;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 28rpx;
+    font-style: normal;
+    font-weight: 400;
+    line-height: normal;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+  }
+  .info-img {
+    width: 32rpx;
+    height: 32rpx;
+    margin-left: 16rpx;
+    background: #0bbc581a;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  .tishi {
+    width: 24rpx;
+    height: 24rpx;
+  }
+  .dev-status {
+    position: absolute;
+    right: 0;
+    top: 0;
+    border: 2rpx solid #ffffff;
+    background: #0bbc581a;
+    width: 152rpx;
+    height: 56rpx;
+    color: #0bbc58;
+    text-align: center;
+    font-size: 28rpx;
+    font-weight: 400;
+    line-height: 52rpx;
+    border-radius: 0 16rpx 0 64rpx;
+  }
+}
+
+/* 标签页切换 */
+.tab-container {
+  background-color: #ffffff;
+  border-radius: 16rpx;
+  overflow: hidden;
+  padding: 0 32rpx;
+  margin-bottom: 24rpx;
+  .tab-top {
+    display: flex;
+  }
+  .tab-item {
+    flex: 1;
+    height: 80rpx;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    position: relative;
+    color: #666666;
+    text-align: center;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 28rpx;
+    font-style: normal;
+    font-weight: 400;
+    line-height: normal;
+    .tab-text {
+      margin-top: 16rpx;
+    }
+    .smile {
+      width: 24rpx;
+      height: 18rpx;
+    }
+  }
+
+  .tab-item.active {
+    color: #333333;
+    font-weight: 700;
+  }
+
+  .time-board {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 16rpx;
+  }
+  .time-filter {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 256rpx;
+    height: 48rpx;
+    padding: 6rpx 8rpx;
+    border-radius: 32rpx;
+    background: #f1f4f8;
+    color: #303133;
+    font-size: 20rpx;
+    font-weight: 400;
+    .filter-item {
+      padding: 4rpx 16rpx;
+      height: 36rpx;
+      line-height: 36rpx;
+    }
+    .active {
+      border-radius: 32rpx;
+      background: #fff;
+    }
+  }
+}
+
+/* 监测要素页面 */
+.monitoring-page {
+  padding-bottom: 20px;
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  .data-card {
+    width: calc(50% - 8rpx);
+    margin-bottom: 16rpx;
+    background-color: #ffffff;
+    border-radius: 16rpx;
+    padding: 16rpx 24rpx;
+    height: 186rpx;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 28rpx;
+    font-weight: 400;
+    box-sizing: border-box;
+    .card-title {
+      color: #042118;
+      margin-bottom: 6rpx;
+    }
+    .data-value-row {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      color: #042118;
+      font-family: 'Source Han Sans CN VF';
+      font-weight: 400;
+      .data-value {
+        font-size: 40rpx;
+        font-weight: 700;
+        margin-right: 8rpx;
+      }
+      .data-unit {
+        font-size: 24rpx;
+      }
+      .device-img {
+        width: 52rpx;
+        height: 52rpx;
+      }
+    }
+    .card-thrid {
+      color: #687a74;
+      margin-top: 6rpx;
+    }
+  }
+}
+/* 设备详情页面 */
+.details-page {
+  .device-params {
+    background-color: #ffffff;
+    border-radius: 16rpx;
+    padding: 32rpx;
+    margin-bottom: 24rpx;
+    .device-control {
+      position: relative;
+      z-index: 3;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 24rpx;
+      .control-left {
+        color: #999999;
+        font-family: 'Source Han Sans CN VF';
+        font-size: 28rpx;
+        display: flex;
+        align-items: center;
+        .reload-img {
+          font-size: 32rpx;
+          color: #0bbc58;
+          margin-left: 14rpx;
+        }
+      }
+      .control-right {
+        display: flex;
+        align-items: center;
+        .control-img {
+          width: 48rpx;
+          height: 48rpx;
+          margin-left: 24rpx;
+        }
+      }
+    }
+    .params-board {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      position: relative;
+      .params-left-container{
+        display: flex;
+        justify-content: space-between;
+      }
+      .params-left {
+        flex:1;
+        display: flex;
+        flex-wrap: wrap;
+        .params-item {
+          display: flex;
+          flex-direction: column;
+          width: 100%;
+          // width: calc(50% - 20rpx);
+          margin-bottom: 24rpx;
+          .params-val {
+            color: #303133;
+            font-size: 28rpx;
+            font-weight: 700;
+            .unit {
+              font-size: 24rpx;
+              font-weight: 400;
+              color:#999999;
+            }
+          }
+          .params-label {
+            color: #999999;
+            font-size: 24rpx;
+            font-weight: 400;
+          }
+        }
+      }
+      .params-right {
+        position: absolute;
+        right: 0;
+        top:0;
+        .device-img {
+          width: 150rpx;
+          height: 300rpx;
+        }
+      }
+    }
+  }
+  .table-container {
+    padding: 16rpx 32rpx 32rpx 32rpx;
+    border-radius: 16rpx;
+    background: #fff;
+    .table-control {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 16rpx;
+      .title {
+        color: #042118;
+        font-family: 'Source Han Sans CN VF';
+        font-size: 28rpx;
+        font-style: normal;
+        font-weight: 700;
+        line-height: normal;
+      }
+      .date-board {
+        display: flex;
+        align-items: center;
+        color: #020305;
+        font-family: 'Source Han Sans CN VF';
+        font-size: 24rpx;
+        font-weight: 400;
+        border-radius: 32rpx;
+        background: #f1f4f8;
+        padding: 16rpx 24rpx;
+        .line {
+          color: #656565;
+          margin: 0 10rpx;
+        }
+        .date-icon {
+          width: 32rpx;
+          height: 32rpx;
+          margin-left: 16rpx;
+        }
+      }
+    }
+    .table-wrap {
+      display: flex;
+      width: 100%;
+      height: 850rpx;
+      box-sizing: border-box;
+      margin: 20rpx 0;
+      color: #042118;
+      font-family: 'Source Han Sans CN VF';
+      font-size: 24rpx;
+      font-style: normal;
+      font-weight: 400;
+      line-height: normal;
+      .fixed-column {
+        width: 250rpx;
+        .table-cell {
+          width: 100%;
+          white-space: nowrap;
+        }
+      }
+      .table-bg {
+        width: 16rpx;
+        background: linear-gradient(270deg, #ffffff33 0%, #9598a433 100%);
+      }
+      .scroll-column {
+        flex: 1;
+        height: auto;
+        overflow: hidden;
+        margin-left: -10rpx;
+      }
+      .scroll-content {
+        min-width: 100%;
+        width: fit-content;
+        display: flex;
+        flex-direction: column;
+        .table-row {
+          display: flex;
+          .table-cell {
+            flex: 1;
+          }
+        }
+      }
+      .table-cell {
+        width: 160rpx;
+        height: 80rpx;
+        line-height: 80rpx;
+        text-align: center;
+        border-bottom: 2px solid #e4e7ed;
+        box-sizing: border-box;
+        overflow: hidden;
+        padding: 0 4rpx;
+        text-overflow: ellipsis;
+      }
+      .table-cell.header {
+        background: #f6f8fc;
+      }
+    }
+
+    .pagination {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-top: 24rpx;
+      .page-item {
+        width: 104rpx;
+        height: 48rpx;
+        color: #656565;
+        font-size: 24rpx;
+        line-height: 48rpx;
+        text-align: center;
+        border: 2rpx solid #e4e7ed;
+        border-radius: 8rpx;
+      }
+      .prev.disabled {
+        color: #c0c4cc;
+        border-color: #e4e7ed;
+      }
+      .next {
+        color: #0bbc58;
+        border: 2px solid #0bbc58;
+      }
+      .next.disabled {
+        color: #c0c4cc;
+        border-color: #e4e7ed;
+      }
+      .page-info {
+        color: #999999;
+        font-family: 'Source Han Sans CN VF';
+        font-size: 24rpx;
+        font-style: normal;
+        font-weight: 400;
+        line-height: normal;
+        .curret-page {
+          color: #303133;
+        }
+      }
+    }
+
+    .empty-state {
+      padding: 80rpx 0;
+    }
+  }
+}
+::v-deep .u-calendar__action {
+  display: flex;
+  justify-content: center;
+}
+</style>

+ 345 - 0
pages/deviceDetails/weatherStation2/simDetail.vue

@@ -0,0 +1,345 @@
+<template>
+  <view class="sim-details-page">
+    <cu-custom :isBack="true">
+      <template slot="content">
+        <view class="nav-title">SIM卡详情</view>
+      </template>
+    </cu-custom>
+    <!-- 加载动画 -->
+    <view v-if="loading" class="loading-container">
+      <u-loading mode="circle"></u-loading>
+      <text class="loading-text">加载中...</text>
+    </view>
+    <!-- 页面内容 -->
+    <view v-else>
+      <view class="sim-first sim-board">
+        <view class="title">数据SIM卡流量</view>
+        <view class="sim-item">
+          <text class="label">ICCID:</text>
+          <span class="val">{{ simData.iccid }}</span>
+          <image
+            @click="copy(simData.iccid)"
+            src="/static/images/device/copy.svg"
+            mode=""
+            class="info-img"
+          ></image>
+        </view>
+        <view class="sim-item">
+          <text class="label">总流量:</text>
+          <span class="val">{{ simData.total }}M</span>
+        </view>
+        <view class="sim-item">
+          <text class="label">状态:</text>
+          <text class="val">{{ simData.status }}</text>
+        </view>
+        <view class="sim-item">
+          <text class="label">到期时间:</text>
+          <text class="val">{{ simData.expire }}</text>
+        </view>
+        <view class="sim-item">
+          <text class="label">厂商名称:</text>
+          <text class="val">{{ simData.company }}</text>
+        </view>
+        <view class="sim-info-count">
+          <view class="sim-item">
+            <text class="label">已使用:</text
+            ><text
+              :style="{
+                color:
+                  simData.used > parseInt(simData.total) ? '#f56c6c' : '#0bbc58',
+              }"
+              >{{ simData.used.toFixed(2) }}M</text
+            >/{{ simData.total }}M
+          </view>
+          <view class="sim-item">
+            <text class="label">剩余:</text>
+            <text
+              class="val"
+              :style="{
+                color: '#0bbc58',
+              }"
+              >{{ (parseInt(simData.total || 0) - simData.used).toFixed(2) }}M</text
+            >
+          </view>
+        </view>
+      </view>
+      <view v-if="simIccidObj.photo_iccid || simIccidObj.is_editor" class="sim-board sim-second">
+        <view class="title">图片SIM卡流量</view>
+        <view class="sim-item">
+          <text class="label">ICCID:</text>
+          <span v-if="!isEditing" class="val">{{ simData.iccid }}</span>
+          <input
+            v-else
+            v-model="editingIccid"
+            type="text"
+            class="edit-input"
+            placeholder="请输入新的ICCID"
+          />
+          <image
+            @click="copy(isEditing ? editingIccid : simData.iccid)"
+            src="/static/images/device/copy.svg"
+            mode=""
+            class="info-img"
+          ></image>
+          <image
+            v-if="!isEditing"
+            @click="toggleEdit"
+            src="/static/images/device/edit.svg"
+            mode=""
+            class="info-img"
+          ></image>
+          <button v-else @click="saveEditIccid" class="save-btn">确定</button>
+        </view>
+      </view>
+      <view class="footer-board">
+        <u-button type="success" class="sub-btn" @click="goBack"
+          >确定</u-button
+        >
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  onLoad(options) {
+    this.loading = true
+    const deviceInfo = JSON.parse(decodeURIComponent(options.deviceInfo));
+    console.log(deviceInfo, "deviceInfo");
+    this.deviceInfo = deviceInfo;
+    this.getIccidByd_id();
+  },
+  data() {
+    return {
+      deviceInfo: {},
+      simData: {
+        iccid: "",
+        total: "",
+        status: "",
+        expire: "",
+        company: "",
+        used: 0,
+      },
+      isEditing: false,
+      editingIccid: "",
+      showSimData: false,
+      empty: false,
+      simIccidObj: {},
+      loading: false,
+    };
+  },
+  methods: {
+    getIccidByd_id() {
+      this.$myRequest({
+        url: "/api/api_gateway?method=forecast.send_control.device_sim_new",
+        data: {
+          d_id: this.deviceInfo.d_id,
+        },
+      }).then((res) => {
+        console.log('getIccidByd_id:', res);
+        if (
+          res.data_iccid ||
+          res.photo_iccid ||
+          res.is_editor
+        ) {
+          console.log(res, '1111')
+          this.empty = false;
+          this.simIccidObj = res;
+          if (res.data_iccid) {
+            this.active = "data_iccid";
+            console.log(222)
+            this.getSimData(res.data_iccid);
+          } else {
+            if (res.photo_iccid || res.is_editor) {
+              this.active = "photo_iccid";
+              if (res.photo_iccid) {
+                this.getSimData(res.photo_iccid);
+              } else {
+                this.loading = false;
+              }
+            }
+          }
+        } else {
+          this.empty = true;
+          this.loading = false;
+        }
+      });
+    },
+    getSimData(iccid) {
+      this.$myRequest({
+        url: "/api/api_gateway?method=forecast.send_control.sim_query_new",
+        data: {
+          iccid: iccid
+        }
+      }).then(res => {
+        console.log('simData:', res)
+        this.loading = false
+        this.simData = res
+        if (res.code) {
+          this.showSimData = false
+          // this.$message.error(res.data.data.msg)
+        } else {
+          this.showSimData = true
+          this.simData = res
+          this.editIccid = res.iccid
+        }
+      })
+    },
+    copy(item) {
+      console.log(item);
+      uni.setClipboardData({
+        data: item,
+        success: function () {
+          console.log("success");
+        },
+      });
+    },
+    toggleEdit() {
+      this.isEditing = !this.isEditing;
+      if (this.isEditing) {
+        this.editingIccid = this.simData.iccid;
+      }
+    },
+    saveIccid() {
+      this.simData.iccid = this.editingIccid;
+      this.isEditing = false;
+    },
+    saveEditIccid() {
+      this.$myRequest({
+        url: '/api/api_gateway?method=forecast.send_control.device_sim_update',
+        data: {
+          d_id: this.deviceInfo.d_id,
+          simid: this.editingIccid
+        }
+      }).then(res => {
+        if (res.code) {
+          this.isEditSim = false
+          this.loading = true
+          // this.$message.success(res.data.data.msg)
+          this.simIccidObj.photo_iccid = this.editingIccid
+          this.getSimData(this.editingIccid)
+        } else {
+          // this.$message.error(res.data.data.msg)
+        }
+        this.isEditing = false;
+      })
+    },
+    goBack() {
+      uni.navigateBack({
+        delta: 1,
+      })
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.sim-details-page {
+  background:
+    linear-gradient(180deg, #ffffff00 0%, #f5f6fa 23.64%, #f5f6fa 100%),
+    linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%);
+  min-height: 100vh;
+  padding: 0 32rpx;
+  padding-top: 98rpx;
+  box-sizing: border-box;
+}
+/* 加载动画样式 */
+.loading-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 60vh;
+  width: 100%;
+}
+.loading-text {
+  font-size: 28rpx;
+  color: #666;
+  margin-top: 32rpx;
+}
+.nav-title{
+  text-align: center;
+}
+.sim-board {
+  padding: 32rpx;
+  border-radius: 16rpx;
+  background: #fff;
+  margin-top: 46rpx;
+  margin-bottom: 24rpx;
+  .title {
+    color: #020305;
+    font-size: 32rpx;
+    font-weight: 700;
+    margin-bottom: 24rpx;
+  }
+  .sim-item {
+    font-size: 28rpx;
+    display: flex;
+    align-items: center;
+    margin-bottom: 24rpx;
+    .label {
+      color: #999999;
+    }
+    .val {
+      color: #303133;
+    }
+    .copy-icon,
+    .edit-icon {
+      width: 32rpx;
+      height: 32rpx;
+    }
+  }
+  .sim-info-count {
+    display: flex;
+    justify-content: space-between;
+  }
+}
+.footer-board {
+  border-top: 2rpx solid #e4e7ed;
+  background: #fff;
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 164rpx;
+  padding: 16rpx 32rpx 0 32rpx;
+  .sub-btn {
+    width: 100%;
+    background: #0bbc58;
+    border-radius: 16rpx;
+  }
+}
+.info-img {
+  width: 32rpx;
+  height: 32rpx;
+  margin-left: 16rpx;
+  background: #0bbc581a;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.edit-input {
+  flex: 1;
+  height: 48rpx;
+  border: 2rpx solid #0bbc58;
+  border-radius: 8rpx;
+  padding: 0 16rpx;
+  font-size: 28rpx;
+  color: #303133;
+  background-color: #f5f7fa;
+}
+.save-btn {
+  margin-left: 16rpx;
+  padding: 8rpx 20rpx;
+  background-color: #0bbc58;
+  color: #fff;
+  border: none;
+  border-radius: 8rpx;
+  font-size: 24rpx;
+  height: 48rpx;
+  line-height: 28rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+</style>

+ 13 - 2
pages/equipList2/index.vue

@@ -704,6 +704,11 @@ import openingDegree from '../assets/openingDegree.png';
 						uni.navigateTo({
 							url: '../cb/sy/detail?detail=' + JSON.stringify(item),
 						});
+						
+						// uni.navigateTo({
+						// 	url: '../sy/detail?info=' +
+						// 		JSON.stringify(item) + '&cmd=sy',
+						// });
 						break;
 					case 2:
 						item.addtime = item.uptime;
@@ -744,8 +749,11 @@ import openingDegree from '../assets/openingDegree.png';
 						obj.lng = item.lng;
 						obj.equip_name = item.name;
 						obj.uptime = item.uptime;
+						// uni.navigateTo({
+						// 	url: '../environment/equipment?shebei=' + JSON.stringify(obj),
+						// });
 						uni.navigateTo({
-							url: '../environment/equipment?shebei=' + JSON.stringify(obj),
+							url: `../deviceDetails/weatherStation2/index?devBid=${item.id}&devName=${item.name}&devStatus=${item.status}&address=${item.address}&uptime=${item.uptime}&d_id=${item.d_id}&deviceType=46`,
 						});
 						break;
 					case 6:
@@ -871,8 +879,11 @@ import openingDegree from '../assets/openingDegree.png';
 						obj.equip_name = item.name;
 						obj.uptime = item.uptime;
 						obj.address = item.address || '无';
+						// uni.navigateTo({
+						// 	url: '../environment/gsequipment?shebei=' + JSON.stringify(obj),
+						// });
 						uni.navigateTo({
-							url: '../environment/gsequipment?shebei=' + JSON.stringify(obj),
+							url: `../deviceDetails/SoilMoisturelist/index?devBid=${item.id}&devName=${item.name}&devStatus=${item.status}&address=${item.address}&uptime=${item.uptime}&d_id=${item.d_id}&deviceType=46`,
 						});
 						break;
 					case 17:

+ 1 - 1
pages/sy/devicePhoto.vue

@@ -392,7 +392,7 @@ export default {
       // 处理label参数生成bugMarkers
       this.processBugMarkers();
 
-      const pest_list = res.pest_list
+      const pest_list = res.pest_list || [];
       const pestArr = new Map()
       const pesAlltList = []
       for(let key in this.pestYype){

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 1
static/images/device/HUMIA-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 4 - 4
static/images/device/ILLUA-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 3
static/images/device/PRECA-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 2 - 2
static/images/device/PRESA-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
static/images/device/SMOID-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
static/images/device/SMOIF-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
static/images/device/SMOIG-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
static/images/device/SMOIH-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
static/images/device/SOMIA-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
static/images/device/SOMIB-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
static/images/device/SOMIC-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
static/images/device/SOPHA-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
static/images/device/STEMD-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
static/images/device/STEMF-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
static/images/device/STEMG-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3 - 0
static/images/device/STEMH-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 1
static/images/device/TEMPA-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 1
static/images/device/WDIRA-default.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 1
static/images/device/WSPDA-default.svg