Browse Source

feat: 新增通用导航头部组件并优化设备详情页布局

- 新增 nav-top-bar 通用导航头部组件,支持固定定位和状态栏占位
- 改造多个设备详情页(sy、bzy、scd、cbd)使用新导航头部,避免内容钻到状态栏下
- 优化 movable-view 样式,显式设置高度解决放大后无法上下拖动问题
- 修复鼠害分析图表空态显示问题,避免无数据时图表持续加载
- 新增鼠害分析日期范围参数,支持按日期筛选数据
- 优化设备控制页,新增滤网更换周期配置(仅孢子仪2.1支持)
- 修复图片查看页双击复位缩放和位置功能,解决视图卡住问题
- 新增图片来源角标,区分监测图像不同方向的图片
- 优化设备列表页,隐藏鼠害设备的上报时间
- 修复操作记录接口权限判断,显式处理无权限错误
- 调整多个页面的样式间距和布局,提升显示效果
- 切换测试环境API地址,更新uview样式引入
allen 4 weeks atrás
parent
commit
f1116ed4fc
45 changed files with 971 additions and 334 deletions
  1. 15 0
      .claude/settings.json
  2. 3 0
      App.vue
  3. 25 11
      components/customCard/customCard.vue
  4. 63 0
      components/nav-top-bar/nav-top-bar.vue
  5. 2 2
      pages/bzy/components/deviceData.vue
  6. 9 3
      pages/bzy/detail.vue
  7. 4 4
      pages/bzy/deviceControl.vue
  8. 2 0
      pages/bzy/devicePhoto.vue
  9. 12 4
      pages/bzy2/components/deviceData.vue
  10. 120 92
      pages/bzy2/detail.vue
  11. 37 4
      pages/bzy2/deviceControl.vue
  12. 2 0
      pages/bzy2/devicePhoto.vue
  13. 2 0
      pages/bzy2/devicePhoto2.vue
  14. 1 1
      pages/cb/shuifeizsFirst/shuifeizs.vue
  15. 28 3
      pages/cb/wenshidp/components/RecordList.vue
  16. 6 2
      pages/cb/wenshidp/wenshidp.vue
  17. 1 1
      pages/cb/zhamenFirst/zhamenzs.vue
  18. 2 2
      pages/cbd/components/deviceData.vue
  19. 10 4
      pages/cbd/detail.vue
  20. 2 0
      pages/cbd/devicePhoto.vue
  21. 19 15
      pages/equipList2/index.vue
  22. 2 2
      pages/scd/components/deviceData.vue
  23. 9 3
      pages/scd/detail.vue
  24. 2 0
      pages/scd/devicePhoto.vue
  25. 2 2
      pages/shuHai/components/deviceData.vue
  26. 3 2
      pages/shuHai/components/photoImage.vue
  27. 69 32
      pages/shuHai/components/rodentAnalysis.vue
  28. 23 3
      pages/shuHai/detail.vue
  29. 8 8
      pages/shuHai/deviceControl.vue
  30. 59 2
      pages/shuHai/devicePhoto.vue
  31. 2 2
      pages/shuHai1/components/deviceData.vue
  32. 129 0
      pages/shuHai1/components/downImage.vue
  33. 1 0
      pages/shuHai1/components/pestDiscern.vue
  34. 47 16
      pages/shuHai1/components/rodentAnalysis.vue
  35. 167 91
      pages/shuHai1/detail.vue
  36. 8 8
      pages/shuHai1/deviceControl.vue
  37. 52 2
      pages/shuHai1/devicePhoto.vue
  38. 2 2
      pages/sy/components/deviceData.vue
  39. 2 2
      pages/sy/components/deviceData2.vue
  40. 9 3
      pages/sy/detail.vue
  41. 2 0
      pages/sy/devicePhoto.vue
  42. 1 1
      pages/warning/index.vue
  43. 4 2
      project.config.json
  44. 2 2
      util/api.js
  45. 1 1
      util/neutral.js

+ 15 - 0
.claude/settings.json

@@ -0,0 +1,15 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(grep -n \"^  watch:\\\\|^  methods:\\\\|^  computed:\\\\|^  data\\(\\)\\\\|^  };$|^  onLoad\\\\|^  async onLoad\" pages/shuHai/detail.vue)",
+      "Bash(awk '/function getPieSeriesData|function pie\\\\\\(|case .pie.:/{print NR\": \"$0}' uni_modules/qiun-data-charts/js_sdk/u-charts/u-charts.js)",
+      "Bash(awk -F: '$1>6400 && $1<6830')",
+      "Bash(awk '/function fixPieSeries/,/^}/' uni_modules/qiun-data-charts/js_sdk/u-charts/u-charts.js)",
+      "Bash(ls g:/project/low_version_bigdata_app/pages 2>/dev/null | head -30; grep -rln \"查看图片\" g:/project/low_version_bigdata_app/pages 2>/dev/null | head)",
+      "Read(//g/project/low_version_bigdata_app/pages/**)",
+      "mcp__zai-mcp-server__analyze_image",
+      "Bash(grep -n 'startDate\\\\|endDate\\\\|$emit\\('\\\\''change'\\\\''' uview-ui/components/u-calendar/u-calendar.vue)",
+      "Bash(grep -n '$emit' uview-ui/components/u-calendar/u-calendar.vue)"
+    ]
+  }
+}

+ 3 - 0
App.vue

@@ -77,6 +77,9 @@ export default {
 };
 </script>
 <style lang="scss">
+/* uview 官方要求的全局样式引入:包含 .u-flex/.u-row-center 等工具类。
+   缺失时 u-calendar 头部(u-flex u-row-center)会竖排错位 */
+@import 'uview-ui/index.scss';
 @import 'style/font/iconfont.css';
 @import './static/iconfont/iconfont.css';
 

+ 25 - 11
components/customCard/customCard.vue

@@ -1,22 +1,36 @@
 <template>
   <view class="custom-card">
-    <view class="custom-card-header">
-      <view class="custom-card-action">
-        <u-icon name="arrow-left" color="#333333" size="32rpx" @tap="BackPage"/>
-        <view class="custom-card-action-text">
-          <slot name="backText"></slot>
+    <!-- 固定头部:滚动时内容不再钻到系统状态栏下面 -->
+    <nav-top-bar :background="background">
+      <view class="custom-card-header">
+        <view class="custom-card-action">
+          <u-icon name="arrow-left" color="#333333" size="32rpx" @tap="BackPage"/>
+          <view class="custom-card-action-text">
+            <slot name="backText"></slot>
+          </view>
+        </view>
+        <view class="cu-bar-right">
+          <slot name="right"></slot>
         </view>
       </view>
-      <view class="cu-bar-right">
-        <slot name="right"></slot>
-      </view>
-    </view>
+    </nav-top-bar>
   </view>
 </template>
 <script>
+import NavTopBar from '@/components/nav-top-bar/nav-top-bar.vue';
+
 export default {
   name: 'custom-card',
-  props: {},
+  components: {
+    NavTopBar,
+  },
+  props: {
+    // 固定栏背景,不传时使用业务页通用渐变,特殊背景页可覆盖
+    background: {
+      type: String,
+      default: 'linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%)',
+    },
+  },
   data() {
     return {};
   },
@@ -33,7 +47,7 @@ export default {
 .custom-card {
   border-radius: 16upx;
   opacity: 1;
-  padding-top: 80rpx;
+  // 状态栏高度由 nav-top-bar 的占位撑起
 }
 
 .custom-card-header {

+ 63 - 0
components/nav-top-bar/nav-top-bar.vue

@@ -0,0 +1,63 @@
+<template>
+	<view>
+		<!-- 固定在顶部:状态栏占位 + 页面自己的头部内容(插槽),滚动时内容从其下方穿过 -->
+		<view class="nav-top-bar" :style="{ background: background }">
+			<uni-status-bar />
+			<view class="nav-top-bar__content">
+				<slot />
+			</view>
+		</view>
+		<!-- 占位:高度自动等于固定栏总高(状态栏 + 插槽内容),保证滚动前布局不变 -->
+		<view class="nav-top-bar__placeholder" :style="{ height: placeholderHeight + 'px' }"></view>
+	</view>
+</template>
+
+<script>
+	import uniStatusBar from '../uni-status-bar/uni-status-bar.vue';
+
+	export default {
+		name: 'nav-top-bar',
+		components: {
+			uniStatusBar
+		},
+		props: {
+			// 固定栏背景(需不透明才能盖住滚上去的内容),可传颜色或渐变
+			background: {
+				type: String,
+				default: '#FFFFFF'
+			}
+		},
+		data() {
+			return {
+				placeholderHeight: 0
+			};
+		},
+		mounted() {
+			this.$nextTick(this.measure);
+		},
+		methods: {
+			// 插槽内容高度变化后(如异步数据渲染出头部)可调用 this.$refs.xxx.measure()
+			measure() {
+				uni.createSelectorQuery().in(this).select('.nav-top-bar').boundingClientRect(rect => {
+					if (rect && rect.height) {
+						this.placeholderHeight = rect.height;
+					}
+				}).exec();
+			}
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	.nav-top-bar {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		z-index: 999;
+	}
+
+	.nav-top-bar__content {
+		width: 100%;
+	}
+</style>

+ 2 - 2
pages/bzy/components/deviceData.vue

@@ -198,9 +198,9 @@
             <text class="sim-value"><text style="color: #0bbc58">{{ deviceInfoStatic.used || ''}}</text>/{{ deviceInfoStatic.total || '' }}</text>
           </view>
           <view class="sim-item">
-            <text class="sim-value">
+            <view class="sim-value">
               <u-line-progress active-color="#0bbc58" :percent="deviceInfoStatic.used / deviceInfoStatic.total * 100" :show-percent="false"></u-line-progress>
-            </text>
+            </view>
           </view>
         </view>
         <view class="sim-container" v-if="activeTab === 1">

+ 9 - 3
pages/bzy/detail.vue

@@ -1,6 +1,8 @@
 <template>
   <view class="device-detail">
-    <view class="device-detail__header">
+    <!-- 固定头部:滚动时内容不再钻到系统状态栏下面 -->
+    <nav-top-bar background="linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%)">
+      <view class="device-detail__header">
       <u-icon
         size="36"
         class="arrow-left"
@@ -8,7 +10,8 @@
         @click="handleBack"
       ></u-icon>
       {{ deviceInfo.name }}
-    </view>
+      </view>
+    </nav-top-bar>
     <view class="operation" @click.stop="isShowOperation = !isShowOperation">操作</view>
     <view class="operation-container" v-if="isShowOperation" @click="closeOperationHandler">
       <view class="operation-background"></view>
@@ -99,6 +102,7 @@
   </view>
 </template>
 <script>
+import NavTopBar from '@/components/nav-top-bar/nav-top-bar.vue';
 import DeviceCard from './components/DeviceCard.vue';
 import PestDiscern from './components/pestDiscern.vue';
 import PestEchart from './components/pestEchart.vue';
@@ -108,6 +112,7 @@ import DeviceData from './components/deviceData.vue';
 
 export default {
   components: {
+    NavTopBar,
     DeviceCard,
     PestDiscern,  
     PestEchart,
@@ -534,7 +539,8 @@ export default {
   display: flex;
   width: 100%;
   height: calc(100vh - 112rpx);
-  padding-top: 112rpx;
+  // 状态栏高度由 nav-top-bar 的占位撑起,这里只留头部与内容的间距
+  padding-top: 24rpx;
   flex-direction: column;
   align-items: center;
   overflow-y: scroll;

+ 4 - 4
pages/bzy/deviceControl.vue

@@ -61,10 +61,10 @@
           <view class="end-time" @click="showPickerHandler(index,'end_time')">{{item.end_time_label || '结束时间'}}
           </view><u-icon name="clock" color="#4E5969" size="26" style="padding-right: 20rpx"></u-icon>
         </view>
-        <view class="device-detail-viewImage" v-if="device_model == 51">
+        <view class="device-detail-viewImage">
           <text class="device-detail-label">高温保护阀值(℃) {{ sliderField === 'tph' && sliderDragValue !== null ? sliderDragValue : equipContrlForm.tph }}</text>
         </view>
-        <view class="slider-container" v-if="device_model == 51">
+        <view class="slider-container">
           <view class="slider-min-value">50</view>
           <view
             class="custom-progress"
@@ -137,10 +137,10 @@
           </view>
           <view class="slider-max-value">40</view>
         </view>
-        <view class="device-detail-viewImage" v-if="device_model == 51">
+        <view class="device-detail-viewImage">
           <text class="device-detail-label">低温保护阀值(℃) {{ sliderField === 'tpl' && sliderDragValue !== null ? sliderDragValue : equipContrlForm.tpl }}</text>
         </view>
-        <view class="slider-container" v-if="device_model == 51">
+        <view class="slider-container">
           <view class="slider-min-value">-30</view>
           <view
             class="custom-progress"

+ 2 - 0
pages/bzy/devicePhoto.vue

@@ -622,6 +622,8 @@ export default {
 
     .movable-view {
       width: 100%;
+      // 显式高度:movable-view 依赖宽高计算各方向拖动边界,高度缺失会导致放大后无法上下拖动
+      height: 100%;
     }
 
     .image-container {

+ 12 - 4
pages/bzy2/components/deviceData.vue

@@ -121,8 +121,12 @@
               <text class="header-cell">设备开关</text>
               <text class="header-cell">环境温度(°C)</text>
               <text class="header-cell">环境湿度(%)</text>
-              <text class="header-cell">电池状态</text>
+              <text class="header-cell">箱内温度</text>
+              <text class="header-cell">电压状态</text>
               <text class="header-cell">工作状态</text>
+              <text class="header-cell">电压(V)</text>
+              <text class="header-cell">电流(mA)</text>
+              <text class="header-cell">信号强度</text>
               <text class="header-cell">设备版本</text>
             </view>
             <view class="scrollable-body">
@@ -136,12 +140,16 @@
                   {{ item.ds == 1 ? '打开' : '关闭' }}</view>
                 <view class="body-cell">{{ item.at }}</view>
                 <view class="body-cell">{{ item.ah }}</view>
+                <view class="body-cell">{{ item.boxtem }}</view>
                 <view class="body-cell">
                   <view class="status-icon" :class="item.batStatus == 0 ? 'success-icon' : 'status-icon'"></view>
                   {{ item.batStatus == 0 ? '正常' : '欠压' }}</view>
                 <view class="body-cell">
                   <view class="status-icon" :class="item.work_sta == 0 ? 'warning-icon' : item.work_sta == 1 ? 'success-icon':'status-icon'"></view>
-                  {{ item.work_sta == 0 ? '待机' : item.work_sta == 1 ? '收集' : item.work_sta == 2 ? '培养' : '拍照' }}</view>
+                  {{ formartWorkSta(item.work_sta) || '--' }}</view>
+                <view class="body-cell">{{ item.vbat }}</view>
+                <view class="body-cell">{{ item.current }}</view>
+                <view class="body-cell">{{ item.csq }}</view>
                 <view class="body-cell">{{ item.dver }}</view>
               </view>
             </view>
@@ -186,9 +194,9 @@
             <text class="sim-value"><text style="color: #0bbc58">{{ deviceInfoStatic.used || ''}}</text>/{{ deviceInfoStatic.total || '' }}</text>
           </view>
           <view class="sim-item">
-            <text class="sim-value">
+            <view class="sim-value">
               <u-line-progress active-color="#0bbc58" :percent="deviceInfoStatic.used / deviceInfoStatic.total * 100" :show-percent="false"></u-line-progress>
-            </text>
+            </view>
           </view>
         </view>
         <view class="sim-container" v-if="activeTab === 1">

+ 120 - 92
pages/bzy2/detail.vue

@@ -1,14 +1,17 @@
 <template>
   <view class="device-detail">
-    <view class="device-detail__header">
-      <u-icon
-        size="36"
-        class="arrow-left"
-        name="arrow-left"
-        @click="handleBack"
-      ></u-icon>
-      {{ deviceInfo.name }}
-    </view>
+    <!-- 固定头部:滚动时内容不再钻到系统状态栏下面 -->
+    <nav-top-bar background="linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%)">
+      <view class="device-detail__header">
+        <u-icon
+          size="36"
+          class="arrow-left"
+          name="arrow-left"
+          @click="handleBack"
+        ></u-icon>
+        {{ deviceInfo.name }}
+      </view>
+    </nav-top-bar>
     <view class="operation" @click.stop="isShowOperation = !isShowOperation">操作</view>
     <view class="operation-container" v-if="isShowOperation" @click="closeOperationHandler">
       <view class="operation-background"></view>
@@ -129,15 +132,20 @@
         <view class="analysis-card">
           <view class="card-title">孢子趋势</view>
           <view class="chart-wrap--spore">
+            <!-- 只在tab激活且有数据时挂载,保证量宽高时容器可见,避免"布局错误" -->
             <qiun-data-charts
-              type="mix"
+              v-if="hasSporeSeries && activeTab === 'pestAnalysis'"
+              type="line"
               :chartData="sporeChart"
               :opts="sporeOpts"
-              :reshow="activeTab === 'pestAnalysis'"
+              :reshow="sporeRedraw"
               :canvas2d="true"
               :inScrollView="true"
               :ontouch="true"
             />
+            <view v-if="!hasSporeSeries" class="chart-empty">
+              <u-empty :text="sporeTrendLoading ? '加载中...' : '暂无数据'"></u-empty>
+            </view>
           </view>
         </view>
       </view>
@@ -147,6 +155,7 @@
   </view>
 </template>
 <script>
+import NavTopBar from '@/components/nav-top-bar/nav-top-bar.vue';
 import DeviceCard from './components/DeviceCard.vue';
 import PestDiscern from './components/pestDiscern.vue';
 import PestEchart from './components/pestEchart.vue';
@@ -157,6 +166,7 @@ import DeviceData from './components/deviceData.vue';
 
 export default {
   components: {
+    NavTopBar,
     DeviceCard,
     PestDiscern,
     PestEchart,
@@ -207,8 +217,12 @@ export default {
       // 孢子分析
       rankColors: ['#4FD8E0', '#FF7A45', '#5AD8A6', '#FFC53D', '#F8E71C', '#5B8FF9', '#E8684A', '#6DC8EC', '#FF9DC6', '#7EDB8F'],
       pestRankList: [],
-      // 各物种每日数量/浓度(char_data)
-      sporeCharData: {},
+      // 孢子趋势数据(bzy_analysis:categories + series)
+      sporeChart: { categories: [], series: [] },
+      // 孢子趋势加载中
+      sporeTrendLoading: false,
+      // 真机首次量宽高偶发为0,延迟翻转此值触发qiun组件重试绘制
+      sporeRedraw: false,
       sporeOpts: {
         dataPointShape: true,
         // 图例置顶,点击可隐藏/显示对应线(同 PC 图例交互)
@@ -226,12 +240,16 @@ export default {
           itemCount: 4,
           scrollShow: true
         },
-        // 双 Y 轴:左轴虫数,右轴浓度
+        // 虫数单轴。注意:opts里的函数属性经小程序setData传给组件后会丢失,不能依赖formatter,
+        // 刻度整数化靠 buildSporeChart 把 data[0].max 取整到 splitNumber 的整数倍
+        // padding给y轴标题和左侧刻度留出空间,避免被裁剪/压住卡片标题
+        padding: [20, 15, 0, 5],
         yAxis: {
           showTitle: true,
           disableGrid: false,
           gridType: 'dash',
           gridColor: '#EEEEEE',
+          splitNumber: 4,
           data: [
             {
               title: '虫数:头',
@@ -239,23 +257,14 @@ export default {
               titleFontColor: '#666666',
               position: 'left',
               min: 0,
-              splitNumber: 4,
-              formatter: val => Math.round(val) + ''
-            },
-            {
-              title: '浓度:个/L',
-              titleFontSize: 10,
-              titleFontColor: '#666666',
-              position: 'right',
-              min: 0,
-              splitNumber: 4,
-              formatter: val => Number(val).toFixed(4)
+              max: 4
             }
           ]
         },
         extra: {
-          mix: {
-            line: { width: 2 }
+          line: {
+            type: 'curve',
+            width: 2
           },
           tooltip: { showBox: true }
         },
@@ -276,6 +285,10 @@ export default {
     // 孢子仪2.1
     isSporeTwo() {
       return this.deviceCode == 1;
+    },
+    // 孢子趋势是否有数据(无数据时显示空状态,避免图表组件一直转loading)
+    hasSporeSeries() {
+      return !!(this.sporeChart && this.sporeChart.series && this.sporeChart.series.length);
     }
   },
   onLoad(options){
@@ -288,6 +301,7 @@ export default {
     // 延迟初始化:等布局稳定后再测 canvas 尺寸
     setTimeout(() => {
       this.getSporeAnalysis();
+      this.getSporeTrend();
     }, 300);
     // this.getPestAnalysis();
     this.isShow();
@@ -341,7 +355,7 @@ export default {
     },
     handleSettingClick(){
       uni.navigateTo({
-        url: '/pages/bzy/deviceControl?deviceId=' + this.deviceInfo.id + '&d_id=' + this.deviceInfo.d_id + '&device_model=' + this.deviceInfo.device_model,
+        url: '/pages/bzy2/deviceControl?deviceId=' + this.deviceInfo.id + '&d_id=' + this.deviceInfo.d_id + '&device_model=' + this.deviceInfo.device_model + '&device_code=' + (this.deviceCode || this.deviceInfo.device_code || ''),
       });
     },
     closeOperationHandler(){
@@ -425,7 +439,8 @@ export default {
       return `${year}-${month}-${day}`;
     },
     formatTime(date){
-      const time = new Date(date).getTime() / 1000;
+      // iOS的JavaScriptCore不支持'YYYY-MM-DD HH:MM:SS'格式,统一替换成'/'分隔保证真机可用
+      const time = new Date(date.replace(/-/g, '/')).getTime() / 1000;
       return time.toFixed(0);
     },
     handleChange(e){
@@ -452,82 +467,85 @@ export default {
         this.getPolylineData();
         this.getDeviceHistoryData();
       }else if(this.activeTab === 'pestAnalysis'){
+        console.log('[bzy2] initAction → pestAnalysis');
         this.getSporeAnalysis();
+        this.getSporeTrend();
       }
     },
     handleTabClick(tab) {
       this.activeTab = tab;
+      // 复位后延迟翻转,触发图表在真机上重试绘制(首次量宽高偶发为0)
+      this.sporeRedraw = false;
       this.initAction();
+      if(tab === 'pestAnalysis'){
+        setTimeout(() => {
+          this.sporeRedraw = true;
+        }, 300);
+      }
     },
-    // 拉取孢子分析数据(虫害识别 + 孢子趋势)
+    // 拉取孢子识别数据(bzy_pest_pie:各物种总量,按数量降序)
     async getSporeAnalysis() {
       const res = await this.$myRequest({
-        url: '/api/api_gateway?method=forecast.bzy.pest_order_and_char',
+        url: '/api/api_gateway?method=forecast.bzy.bzy_pest_pie',
         method: 'POST',
         data: {
-          id: this.deviceInfo.d_id,
-          hour_st: '00',
-          hour_ed: '00',
-          device_type_id: this.deviceInfo.type_id || this.deviceInfo.type,
-          start: this.formatTime(this.startDate + ' 00:00:00'),
-          end: this.formatTime(this.endDate + ' 23:59:59')
+          device_id: this.deviceInfo.id,
+          start: this.formatTime(this.startDate + ' 00:00:00'),// 秒级时间戳
+          end: this.formatTime(this.endDate + ' 23:59:59')// 秒级时间戳
         }
       });
-      // 虫害识别:pest_total 按数量降序
-      const pestTotal = (res && res.pest_total) || {};
-      this.pestRankList = Object.keys(pestTotal)
-        .map(name => ({ name: name, count: pestTotal[name] || 0 }))
+      const list = (res && res.data) || [];
+      this.pestRankList = list
+        .map(item => ({ name: item.name, count: item.value || 0 }))
         .sort((a, b) => b.count - a.count);
-      // 各物种每日数量/浓度(char_data)
-      const charData = (res && res.char_data) || {};
-      const charRows = {};
-      Object.keys(charData).forEach(name => {
-        charRows[name] = (charData[name] || []).map(item => ({
-          date: this.formatDay(item.addtime),
-          value: item.sum || 0,
-          ctn: item.ctn || 0
-        }));
-      });
-      this.sporeCharData = charRows;
-      this.buildSporeChart();
-    },
-    // 秒级时间戳 → YYYY-MM-DD
-    formatDay(seconds) {
-      return this.formatDate(new Date(Number(seconds) * 1000));
     },
-    // 孢子趋势:所有物种的数量(左轴)+ 浓度(右轴)画在同一张图,点图例可隐藏/显示
-    buildSporeChart() {
-      const names = Object.keys(this.sporeCharData);
-      // 汇总所有日期作为 x 轴
-      const categories = [];
-      names.forEach(name => {
-        this.sporeCharData[name].forEach(row => {
-          if (categories.indexOf(row.date) === -1) categories.push(row.date);
-        });
-      });
-      categories.sort();
-      const series = [];
-      names.forEach(name => {
-        const map = {};
-        this.sporeCharData[name].forEach(row => {
-          map[row.date] = row;
-        });
-        series.push({
-          name: name,
-          type: 'line',
-          style: 'curve',
-          index: 0,
-          data: categories.map(d => (map[d] ? map[d].value : null))
+    // 孢子趋势:各物种每日数量(秒级时间戳)
+    async getSporeTrend() {
+      this.sporeTrendLoading = true;
+      try {
+        console.log('[bzy2] getSporeTrend start', this.deviceInfo.id, this.startDate, this.endDate);
+        const res = await this.$myRequest({
+          url: '/api/api_gateway?method=forecast.bzy.bzy_analysis',
+          method: 'POST',
+          data: {
+            device_id: this.deviceInfo.id,
+            start: this.formatTime(this.startDate + ' 00:00:00'),// 秒级时间戳
+            end: this.formatTime(this.endDate + ' 23:59:59')// 秒级时间戳
+          }
         });
-        series.push({
-          name: name + '浓度',
-          type: 'line',
-          style: 'curve',
-          index: 1,
-          data: categories.map(d => (map[d] ? map[d].ctn : null))
+        // $myRequest解包后应为{categories, series};若拿到的是完整返回体则取data字段
+        const payload = res && res.series ? res : ((res && res.data) || {});
+        console.log(payload,'payloadpayload');
+        console.log('[bzy2] bzy_analysis payload:', JSON.stringify(payload).slice(0, 300));
+        this.buildSporeChart(payload);
+      } catch (e) {
+        console.log('[bzy2] bzy_analysis error:', e);
+      } finally {
+        this.sporeTrendLoading = false;
+      }
+    },
+    // 孢子趋势:categories/series → 折线图;左轴max取整到splitNumber的整数倍,保证虫数刻度无小数
+    buildSporeChart(res) {
+      const categories = res?.categories || [];
+      const series = (res?.series || []).map(item => ({
+        name: item.name,
+        data: (item.data || []).map(v => v || 0)
+      }));
+      let maxCount = 0;
+      series.forEach(item => {
+        item.data.forEach(v => {
+          if (v > maxCount) maxCount = v;
         });
       });
+      this.sporeOpts.yAxis.data[0].max = Math.max(4, Math.ceil(maxCount / 4) * 4);
       this.sporeChart = { categories: categories, series: series };
+      // 真机上挂载后首次量宽高偶发为0,稍后翻转reshow触发组件重试绘制
+      this.sporeRedraw = false;
+      this.$nextTick(() => {
+        setTimeout(() => {
+          this.sporeRedraw = true;
+        }, 400);
+      });
     },
     // 虫害识别进度条宽度:占总量百分比,保底 4% 便于查看
     rankPercent(item) {
@@ -795,11 +813,12 @@ export default {
 .device-detail {
   display: flex;
   width: 100%;
-  height: calc(100vh - 112rpx);
-  padding-top: 112rpx;
+  // 用页面原生滚动:CSS容器滚动(overflow)会导致canvas同层渲染在滚动时错位
+  min-height: calc(100vh - 24rpx);
+  // 状态栏高度由 nav-top-bar 的占位撑起,这里只留头部与内容的间距
+  padding-top: 24rpx;
   flex-direction: column;
   align-items: center;
-  overflow-y: scroll;
 
   // 孢子分析
   .spore-analysis {
@@ -862,6 +881,12 @@ export default {
     .chart-wrap--spore {
       width: 100%;
       height: 420rpx;
+      .chart-empty {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 100%;
+      }
     }
   }
 
@@ -886,7 +911,6 @@ export default {
     margin: 0 auto;
     border-radius: 16rpx;
     overflow-x: hidden;
-    overflow-y: auto;
     // 隐藏滚动条
     -ms-overflow-style: none;
     scrollbar-width: none;
@@ -937,7 +961,8 @@ export default {
     .tabs-timer-container{
       display: flex;
       align-items: center;
-      width: calc(100% - 256rpx);
+      justify-content: center;
+      width: calc(100% - 206rpx);
       height: 64rpx;
       line-height: 64rpx;
       text-align: center;
@@ -950,8 +975,10 @@ export default {
       color: #656565;
       margin: 16rpx;
       position: relative;
+      white-space: nowrap;
       .tabs-timer-item{
-        width: 42%;
+        width: auto;
+        flex-shrink: 0;
         color: #656565;
         text-align: center;
         font-family: "Source Han Sans CN VF";
@@ -960,6 +987,7 @@ export default {
       }
       .calendar-icon{
         margin-left: 24rpx;
+        flex-shrink: 0;
       }
     }
     .tab-item {

+ 37 - 4
pages/bzy2/deviceControl.vue

@@ -61,10 +61,10 @@
           <view class="end-time" @click="showPickerHandler(index,'end_time')">{{item.end_time_label || '结束时间'}}
           </view><u-icon name="clock" color="#4E5969" size="26" style="padding-right: 20rpx"></u-icon>
         </view>
-        <view class="device-detail-viewImage" v-if="device_model == 51">
+        <view class="device-detail-viewImage">
           <text class="device-detail-label">高温保护阀值(℃) {{ sliderField === 'tph' && sliderDragValue !== null ? sliderDragValue : equipContrlForm.tph }}</text>
         </view>
-        <view class="slider-container" v-if="device_model == 51">
+        <view class="slider-container">
           <view class="slider-min-value">50</view>
           <view
             class="custom-progress"
@@ -137,10 +137,10 @@
           </view>
           <view class="slider-max-value">40</view>
         </view>
-        <view class="device-detail-viewImage" v-if="device_model == 51">
+        <view class="device-detail-viewImage">
           <text class="device-detail-label">低温保护阀值(℃) {{ sliderField === 'tpl' && sliderDragValue !== null ? sliderDragValue : equipContrlForm.tpl }}</text>
         </view>
-        <view class="slider-container" v-if="device_model == 51">
+        <view class="slider-container">
           <view class="slider-min-value">-30</view>
           <view
             class="custom-progress"
@@ -156,6 +156,26 @@
           </view>
           <view class="slider-max-value">20</view>
         </view>
+        <!-- 滤网更换周期:仅孢子仪2.1(device_code=1)支持 -->
+        <view class="device-detail-viewImage" v-if="device_code == 1">
+          <text class="device-detail-label">滤网更换周期(天) {{ sliderField === 'freq_filter' && sliderDragValue !== null ? sliderDragValue : equipContrlForm.freq_filter }}</text>
+        </view>
+        <view class="slider-container" v-if="device_code == 1">
+          <view class="slider-min-value">1</view>
+          <view
+            class="custom-progress"
+            @touchstart.stop="onSliderTouchStart($event, 'freq_filter', 1, 30, 1)"
+            @touchmove.stop.prevent="onSliderTouchMove($event, 1, 30, 1)"
+            @touchend.stop="onSliderTouchEnd"
+            @tap.stop="onSliderTap($event, 'freq_filter', 1, 30, 1)"
+          >
+            <view class="progress-track">
+              <view class="progress-fill" :style="{ width: getProgressWidth(sliderField === 'freq_filter' && sliderDragValue !== null ? sliderDragValue : equipContrlForm.freq_filter, 1, 30) + '%' }"></view>
+              <view class="progress-thumb" :style="{ left: getProgressWidth(sliderField === 'freq_filter' && sliderDragValue !== null ? sliderDragValue : equipContrlForm.freq_filter, 1, 30) + '%' }"></view>
+            </view>
+          </view>
+          <view class="slider-max-value">30</view>
+        </view>
         <view class="device-detail-viewImage">
           <text class="device-detail-label">数据上传间隔(min) {{ sliderField === 'datt' && sliderDragValue !== null ? sliderDragValue : equipContrlForm.datt }}</text>
         </view>
@@ -237,12 +257,14 @@ export default {
       sliderMin: 0,
       sliderMax: 100,
       device_model: '',
+      device_code: '',
       sliderStep: 1
     }
   },
   onLoad(options){
     this.d_id = options.d_id;
     this.device_model = options.device_model;
+    this.device_code = options.device_code || '';
     this.getControlDeviceConfigInfo();
     this.timeList = [];
     for(let i = 0;i<24;i++){
@@ -502,10 +524,17 @@ export default {
       newForm.on_off = Number(newForm.on_off);
       newForm.tpl = Number(newForm.tpl);
       newForm.tph = Number(newForm.tph);
+      newForm.freq_filter = Number(newForm.freq_filter);
       if(this.device_model == 0){
         delete newForm.tpl
         delete newForm.tph
       }
+      // 滤网更换周期仅孢子仪2.1(device_code=1)支持
+      if(this.device_code == 1){
+        newForm.freq_filter = Number(newForm.freq_filter);
+      }else{
+        delete newForm.freq_filter
+      }
       this.$myRequest({
         url: '/api/api_gateway?method=forecast.send_control.device_control',
         method: 'POST',
@@ -539,6 +568,10 @@ export default {
         },
       });
       this.equipContrlForm = res
+      // 2.1 设备的滤网更换周期缺省按 1 天显示
+      if(this.device_code == 1){
+        this.equipContrlForm.freq_filter = parseInt(this.equipContrlForm.freq_filter || 1);
+      }
       const coll_time = res?.coll_time || [];
       for(let i = 0;i< coll_time.length;i++){
         const coll_item = coll_time[i];

+ 2 - 0
pages/bzy2/devicePhoto.vue

@@ -820,6 +820,8 @@ export default {
 
     .movable-view {
       width: 100%;
+      // 显式高度:movable-view 依赖宽高计算各方向拖动边界,高度缺失会导致放大后无法上下拖动
+      height: 100%;
     }
 
     .image-container {

+ 2 - 0
pages/bzy2/devicePhoto2.vue

@@ -791,6 +791,8 @@ export default {
 
     .movable-view {
       width: 100%;
+      // 显式高度:movable-view 依赖宽高计算各方向拖动边界,高度缺失会导致放大后无法上下拖动
+      height: 100%;
     }
 
     .image-container {

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

@@ -590,7 +590,7 @@ uni-page-body {
     padding: 2rpx;
     background: linear-gradient(180deg, #FFF 0%, #F5F6FA 100%);
     .irrMode__content-header{
-      width: calc(100% - 32rpx);
+      width: 100%;
       padding: 16rpx 16rpx 16rpx 16rpx;
       border-radius: 12rpx;
       background: linear-gradient(180deg, #BBF1EA 0%, #E6F4F9 100%);

+ 28 - 3
pages/cb/wenshidp/components/RecordList.vue

@@ -89,8 +89,14 @@ export default {
       return this.total > this.pageNum * this.pageSize;
     },
   },
-  created() {
-    this.getRecordList();
+  watch: {
+    // devBid 就绪后再拉取,避免父级赋值时机晚于组件创建时空查
+    devBid: {
+      immediate: true,
+      handler(val) {
+        if (val) this.getRecordList();
+      },
+    },
   },
   methods: {
     async getRecordList() {
@@ -103,6 +109,7 @@ export default {
           header: {
             'Content-Type': 'application/json',
           },
+          sfType: true,
           data: {
             devBid: this.devBid,
             startTime: this.startTime + ' 00:00:00',
@@ -111,8 +118,26 @@ export default {
             pageSize: this.pageSize,
           },
         });
-        this.recordList = this.recordList.concat(res?.data || []);
+        // 该服务失败时(如 token 无权限返回 {code:403, msg:'无权限访问'}),
+        // $myRequest 会把完整错误体 resolve 出来且不弹任何提示,这里显式校验
+        if (res?.code && res.code !== '000000') {
+          uni.showToast({
+            title: res.msg || '查询失败',
+            icon: 'none',
+            duration: 3000,
+          });
+          return;
+        }
+        this.recordList = this.recordList.concat(
+          Array.isArray(res?.data) ? res.data : []
+        );
         this.total = res?.total || 0;
+      } catch (e) {
+        uni.showToast({
+          title: '获取操作记录失败',
+          icon: 'none',
+          duration: 3000,
+        });
       } finally {
         this.loading = false;
       }

+ 6 - 2
pages/cb/wenshidp/wenshidp.vue

@@ -72,6 +72,9 @@ export default {
   },
   data() {
     return {
+      // 必须在 data 中声明:模板 :devBid 传参走视图层数据,
+      // 仅在 onLoad 里挂到 this 上时子组件拿到的是空串
+      devBid: '',
       location:
         'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/image/zhongshui/location.svg',
       ws: null,
@@ -399,8 +402,9 @@ uni-page-body {
     padding: 0 30rpx;
   }
   .greenhouse-bottom {
-    width: calc(100% - 128rpx);
-    padding: 32rpx 32rpx 1rpx 32rpx;
+    width: calc(100% - 68rpx);
+    /* 底部用真实 padding,不依赖最后一个子卡片的 margin(真机上 margin 会被吞掉,白边消失) */
+    padding: 32rpx;
     background: #ffffff;
     border-radius: 16rpx;
     margin: 0 auto;

+ 1 - 1
pages/cb/zhamenFirst/zhamenzs.vue

@@ -506,7 +506,7 @@ uni-page-body {
     padding: 2rpx;
     background: linear-gradient(180deg, #FFF 0%, #F5F6FA 100%);
     .irrMode__content-header{
-      width: calc(100% - 32rpx);
+      width: 100%;
       padding: 16rpx 16rpx 16rpx 16rpx;
       border-radius: 12rpx;
       background: linear-gradient(180deg, #BBF1EA 0%, #E6F4F9 100%);

+ 2 - 2
pages/cbd/components/deviceData.vue

@@ -203,9 +203,9 @@
             <text class="sim-value"><text style="color: #0BBC58">{{ deviceInfoStatic.used || ''}}</text>/{{ deviceInfoStatic.total || '' }}</text>
           </view>
           <view class="sim-item">
-            <text class="sim-value">
+            <view class="sim-value">
               <u-line-progress active-color="#0BBC58" :percent="deviceInfoStatic.used / deviceInfoStatic.total * 100" :show-percent="false"></u-line-progress>
-            </text>
+            </view>
           </view>
         </view>
         <view class="sim-container" v-if="activeTab === 1">

+ 10 - 4
pages/cbd/detail.vue

@@ -1,6 +1,8 @@
 <template>
   <view class="device-detail">
-    <view class="device-detail__header">
+    <!-- 固定头部:滚动时内容不再钻到系统状态栏下面 -->
+    <nav-top-bar background="linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%)">
+      <view class="device-detail__header">
       <u-icon
         size="36"
         class="arrow-left"
@@ -8,7 +10,8 @@
         @click="handleBack"
       ></u-icon>
       {{ deviceInfo.name }}
-    </view>
+      </view>
+    </nav-top-bar>
     <view class="operation" @click.stop="isShowOperation = !isShowOperation">操作</view>
     <view class="operation-container" v-if="isShowOperation" @click="closeOperationHandler">
       <view class="operation-background"></view>
@@ -133,6 +136,7 @@
   </view>
 </template>
 <script>
+import NavTopBar from '@/components/nav-top-bar/nav-top-bar.vue';
 import DeviceCard from './components/DeviceCard.vue';
 import PestDiscern from './components/pestDiscern.vue';
 import PestEchart from './components/pestEchart.vue';
@@ -143,6 +147,7 @@ import AiChat from './components/aiChat.vue';
 
 export default {
   components: {
+    NavTopBar,
     DeviceCard,
     PestDiscern,
     AiChat,
@@ -617,8 +622,9 @@ export default {
 .device-detail {
   display: flex;
   width: 100%;
-  min-height: calc(100vh - 112rpx);
-  padding-top: 112rpx;
+  min-height: calc(100vh - 24rpx);
+  // 状态栏高度由 nav-top-bar 的占位撑起,这里只留头部与内容的间距
+  padding-top: 24rpx;
   flex-direction: column;
   align-items: center;
   background: linear-gradient(180deg, #ffffff00 0%, #F5F6FA 23.64%, #F5F6FA 100%), linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%);

+ 2 - 0
pages/cbd/devicePhoto.vue

@@ -701,6 +701,8 @@ export default {
 
     .movable-view {
       width: 100%;
+      // 显式高度:movable-view 依赖宽高计算各方向拖动边界,高度缺失会导致放大后无法上下拖动
+      height: 100%;
     }
 
     .image-container {

+ 19 - 15
pages/equipList2/index.vue

@@ -93,7 +93,7 @@
 							<view class="p1">
                 <text class="label">设备ID</text>{{ item.only_for_show || item.id || item.device_id }}
               </view>
-							<view class="p1">
+							<view class="p1" v-if="!isRodentDevice(item)">
                 <text class="label">上报时间</text>{{ (item.report || item.uptime || item.upload_time) | timeFormat() }}
               </view>
 							<view class="p1" style="display:flex;align-items: center;">
@@ -517,6 +517,10 @@ import { wgs84togcj02, bd09togcj02 } from '../../util/coordtransform_utils.js';
 			isShow(item){
 				return item.pur_id != 422
 			},
+			// 鼠害监测系统1.0(31)/2.0(51) 的设备卡片不显示上报时间
+			isRodentDevice(item){
+				return item.type_id == 31 || item.type_id == 51;
+			},
 			getSlice(name){
 				return name.slice(0,4)
 			},
@@ -1045,20 +1049,20 @@ import { wgs84togcj02, bd09togcj02 } from '../../util/coordtransform_utils.js';
 							url: '../cb/xylps/detail/detail?detail=' + JSON.stringify(item),
 						});
 						break;
-					// case 51:
-					// 	item.addtime = item.uptime;
-					// 	item.type = item.type_id;
-					// 	uni.navigateTo({
-					// 		url: '../shuHai/detail?info=' + JSON.stringify(item),
-					// 	});
-					// 	break;
-					// case 31:
-					// 	item.addtime = item.uptime;
-					// 	item.type = item.type_id;
-					// 	uni.navigateTo({
-					// 		url: '../shuHai1/detail?info=' + JSON.stringify(item),
-					// 	});
-					// 	break;
+					case 51:
+						item.addtime = item.uptime;
+						item.type = item.type_id;
+						uni.navigateTo({
+							url: '../shuHai/detail?info=' + JSON.stringify(item),
+						});
+						break;
+					case 31:
+						item.addtime = item.uptime;
+						item.type = item.type_id;
+						uni.navigateTo({
+							url: '../shuHai1/detail?info=' + JSON.stringify(item),
+						});
+						break;
 					case 40:
 						item.addtime = item.uptime;
 						uni.navigateTo({

+ 2 - 2
pages/scd/components/deviceData.vue

@@ -206,9 +206,9 @@
             <text class="sim-value"><text style="color: #0bbc58">{{ deviceInfoStatic.used || ''}}</text>/{{ deviceInfoStatic.total || '' }}</text>
           </view>
           <view class="sim-item">
-            <text class="sim-value">
+            <view class="sim-value">
               <u-line-progress active-color="#0bbc58" :percent="deviceInfoStatic.used / deviceInfoStatic.total * 100" :show-percent="false"></u-line-progress>
-            </text>
+            </view>
           </view>
         </view>
         <view class="sim-container" v-if="activeTab === 1">

+ 9 - 3
pages/scd/detail.vue

@@ -1,6 +1,8 @@
 <template>
   <view class="device-detail">
-    <view class="device-detail__header">
+    <!-- 固定头部:滚动时内容不再钻到系统状态栏下面 -->
+    <nav-top-bar background="linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%)">
+      <view class="device-detail__header">
       <u-icon
         size="36"
         class="arrow-left"
@@ -8,7 +10,8 @@
         @click="handleBack"
       ></u-icon>
       {{ deviceInfo.name }}
-    </view>
+      </view>
+    </nav-top-bar>
     <view class="operation" @click.stop="isShowOperation = !isShowOperation">操作</view>
     <view class="operation-container" v-if="isShowOperation" @click="closeOperationHandler">
       <view class="operation-background"></view>
@@ -103,6 +106,7 @@
   </view>
 </template>
 <script>
+import NavTopBar from '@/components/nav-top-bar/nav-top-bar.vue';
 import DeviceCard from './components/DeviceCard.vue';
 import PestDiscern from './components/pestDiscern.vue';
 import PestEchart from './components/pestEchart.vue';
@@ -112,6 +116,7 @@ import DeviceData from './components/deviceData.vue';
 
 export default {
   components: {
+    NavTopBar,
     DeviceCard,
     PestDiscern,  
     PestEchart,
@@ -559,7 +564,8 @@ export default {
   display: flex;
   width: 100%;
   height: calc(100vh - 112rpx);
-  padding-top: 112rpx;
+  // 状态栏高度由 nav-top-bar 的占位撑起,这里只留头部与内容的间距
+  padding-top: 24rpx;
   flex-direction: column;
   align-items: center;
   overflow-y: scroll;

+ 2 - 0
pages/scd/devicePhoto.vue

@@ -620,6 +620,8 @@ export default {
 
     .movable-view {
       width: 100%;
+      // 显式高度:movable-view 依赖宽高计算各方向拖动边界,高度缺失会导致放大后无法上下拖动
+      height: 100%;
     }
 
     .image-container {

+ 2 - 2
pages/shuHai/components/deviceData.vue

@@ -165,9 +165,9 @@
             <text class="sim-value"><text style="color: #0BBC58">{{ deviceInfoStatic.used || ''}}</text>/{{ deviceInfoStatic.total || '' }}</text>
           </view>
           <view class="sim-item">
-            <text class="sim-value">
+            <view class="sim-value">
               <u-line-progress active-color="#0BBC58" :percent="deviceInfoStatic.used / deviceInfoStatic.total * 100" :show-percent="false"></u-line-progress>
-            </text>
+            </view>
           </view>
         </view>
         <view class="sim-container" v-if="activeTab === 1">

+ 3 - 2
pages/shuHai/components/photoImage.vue

@@ -1,6 +1,7 @@
 <template>
   <view class="photo-image">
-    <view class="photo-tabs">
+    <!-- 当前日期范围无数据时隐藏设备切换标签,避免空数据下仍显示一排标签 -->
+    <view class="photo-tabs" v-if="images.length">
       <view class="photo-tab-item" :class="{ active: activeTab === 0 }" @click="changeTabActive(0)">全部</view>
       <view class="photo-tab-item" :class="{ active: activeTab === 1 }" @click="changeTabActive(1, ding_one_device_id)">{{ ding_one_device_id }}</view>
       <view class="photo-tab-item" :class="{ active: activeTab === 2 }" @click="changeTabActive(2, ding_two_device_id)">{{ ding_two_device_id }}</view>
@@ -8,7 +9,7 @@
     </view>
     <view
       class="photo-image__tabs"
-      v-if="disableShow"
+      v-if="disableShow && images.length"
     >
       <view
         class="photo-image__tab"

+ 69 - 32
pages/shuHai/components/rodentAnalysis.vue

@@ -31,7 +31,7 @@
         </view>
       </view>
       <view class="trend-axis-label">鼠害数量:只</view>
-      <view class="chart-wrap chart-wrap--trend">
+      <view class="chart-wrap chart-wrap--trend" v-if="!trendEmpty">
         <qiun-data-charts
           type="line"
           :chartData="trendChart"
@@ -42,6 +42,7 @@
           :ontouch="true"
         />
       </view>
+      <view v-else class="chart-empty">暂无数据</view>
     </view>
 
     <!-- 鼠害活动时段分析 -->
@@ -57,7 +58,7 @@
         <view class="chart-legend__dot chart-legend__dot--bar"></view>
         <text class="chart-legend__text">鼠害数量</text>
       </view>
-      <view class="chart-wrap chart-wrap--rhythm">
+      <view class="chart-wrap chart-wrap--rhythm" v-if="!rhythmEmpty">
         <qiun-data-charts
           type="column"
           :chartData="rhythmChart"
@@ -68,6 +69,7 @@
           :ontouch="true"
         />
       </view>
+      <view v-else class="chart-empty">暂无数据</view>
     </view>
 
     <!-- 鼠害占比 -->
@@ -75,7 +77,7 @@
       <view class="analysis-card__header">
         <view class="analysis-card__title">鼠害占比</view>
       </view>
-      <view class="community-content">
+      <view class="community-content" v-if="!communityEmpty">
         <view class="chart-wrap chart-wrap--ring">
           <qiun-data-charts
             type="ring"
@@ -110,6 +112,7 @@
           </view>
         </view>
       </view>
+      <view v-else class="chart-empty">暂无数据</view>
     </view>
 
     <!-- 鼠情发生量与气象关系 -->
@@ -134,7 +137,7 @@
           >年</view>
         </view> -->
       </view>
-      <view class="chart-wrap chart-wrap--weather">
+      <view class="chart-wrap chart-wrap--weather" v-if="!weatherEmpty">
         <qiun-data-charts
           type="mix"
           :chartData="weatherChart"
@@ -145,6 +148,7 @@
           :ontouch="true"
         />
       </view>
+      <view v-else class="chart-empty">暂无数据</view>
     </view>
   </view>
 </template>
@@ -167,17 +171,27 @@ export default {
       type: Boolean,
       default: true
     },
-    // 设备 id:发生趋势 / 气象关系接口入参
+    // 设备 id:发生趋势 / 气象关系由页面日期选择驱动
     deviceId: {
       type: [String, Number],
       default: ''
+    },
+    // 查询范围(秒级时间戳),由页面日期选择驱动
+    start: {
+      type: [String, Number],
+      default: 0
+    },
+    end: {
+      type: [String, Number],
+      default: 0
     }
   },
   data() {
     return {
       // 发生趋势
-      trendRange: 'month',
       trendChart: null,
+      // 无数据空态:为 true 时不渲染对应图表(空 series 会让 qiun-data-charts 一直转 loading)
+      trendEmpty: false,
       trendOpts: {
         // 颜色顺序与 series 一一对应
         color: ['#FF5252', '#0BBC58', '#3D5AFE'],
@@ -219,6 +233,7 @@ export default {
       // 活动节律
       peakRange: '--',
       rhythmChart: null,
+      rhythmEmpty: false,
       rhythmOpts: {
         color: ['#FFC107'],
         dataLabel: false,
@@ -264,6 +279,7 @@ export default {
       // 当前选中的占比列表项索引(-1 为未选中)
       communitySelected: -1,
       communityChart: null,
+      communityEmpty: false,
       communityOpts: {
         color: ['#E53935', '#FFC107', '#2196F3', '#9E9E9E'],
         // 点击联动会触发整图重绘(canvas 无局部更新),关掉动画避免每次点击重放入场动画
@@ -305,9 +321,9 @@ export default {
       alarmThreshold: 100,
       barMax: 130,
 
-      // 鼠情与气象(mock)
-      weatherRange: 'month',
+      // 鼠情与气象
       weatherChart: null,
+      weatherEmpty: false,
       weatherOpts: {
         color: ['#FF5252', '#FFC107', '#2196F3'],
         dataLabel: false,
@@ -369,6 +385,10 @@ export default {
     };
   },
   computed: {
+    // 设备或日期范围任一变化即触发重拉
+    fetchKey() {
+      return [this.deviceId, this.start, this.end].join('|');
+    },
     // 趋势图图例:跟随接口返回的物种系列
     trendLegend() {
       if (!this.trendChart) return [];
@@ -390,18 +410,15 @@ export default {
     }, 300);
   },
   watch: {
-    // 设备信息晚于组件挂载到达时重新拉取
-    deviceId(val) {
-      if (!val) return;
-      this.fetchAnalysis(this.trendRange, 'trend');
-      this.fetchAnalysis(this.weatherRange, 'weather');
-      this.fetchPeriod();
+    // 设备或日期范围变化时重新拉取
+    fetchKey() {
+      this.initChartData();
     }
   },
   methods: {
     initChartData() {
-      this.fetchAnalysis(this.trendRange, 'trend');
-      this.fetchAnalysis(this.weatherRange, 'weather');
+      this.fetchAnalysis('trend');
+      this.fetchAnalysis('weather');
       this.fetchPeriod();
     },
     switchRange(range) {
@@ -415,25 +432,26 @@ export default {
       this.fetchAnalysis(range, 'trend');
     },
     // 拉取按天分析数据,chartKey 区分构建趋势图还是气象图
-    async fetchAnalysis(range, chartKey) {
-      if (!this.deviceId) return;
-      const { start, end } = this.getRangeTime(range);
+    async fetchAnalysis(chartKey) {
+      if (!this.deviceId || !this.start || !this.end) return;
       try {
         const res = await this.$myRequest({
           url: ANALYSIS_URL,
           method: 'POST',
           data: {
             device_id: this.deviceId,
-            start_time: start,
-            end_time: end
+            start_time: this.start,
+            end_time: this.end
           }
         });
         const dayList = (res && res.day_list) || [];
-        if (!dayList.length) return;
         if (chartKey === 'trend') {
-          this.trendChart = this.buildTrendChart(dayList);
+          // 无数据时清空旧图,显示空态(不再保留上一次的数据)
+          this.trendEmpty = !dayList.length;
+          this.trendChart = dayList.length ? this.buildTrendChart(dayList) : null;
         } else {
-          this.weatherChart = this.buildWeatherChart(dayList);
+          this.weatherEmpty = !dayList.length;
+          this.weatherChart = dayList.length ? this.buildWeatherChart(dayList) : null;
         }
       } catch (e) {
         // $myRequest 已统一 toast 错误信息
@@ -491,24 +509,24 @@ export default {
       return {
         categories: dayList.map(day => (day.date || '').slice(5)),
         series: [
-          { name: '鼠害总数', type: 'area', style: 'curve', index: 0, addPoint: single, data: dayList.map(day => day.total || 0) },
+          // area 系列图例默认画三角形,指定 legendShape 为 line(15x4 的扁长方形),与温度/湿度的图例同高
+          { name: '鼠害总数', type: 'area', style: 'curve', index: 0, addPoint: single, legendShape: 'line', data: dayList.map(day => day.total || 0) },
           { name: '温度', type: 'line', style: 'curve', index: 1, addPoint: single, data: dayList.map(day => day.temperature || 0) },
           { name: '湿度', type: 'line', style: 'curve', index: 1, addPoint: single, data: dayList.map(day => day.humidity || 0) }
         ]
       };
     },
-    // 拉取时段分析数据(活动时段 + 鼠害占比共用),默认统计近 30 天
+    // 拉取时段分析数据(活动时段 + 鼠害占比共用)
     async fetchPeriod() {
-      if (!this.deviceId) return;
-      const { start, end } = this.getRangeTime('month');
+      if (!this.deviceId || !this.start || !this.end) return;
       try {
         const res = await this.$myRequest({
           url: PERIOD_URL,
           method: 'POST',
           data: {
             device_id: this.deviceId,
-            start_time: start,
-            end_time: end
+            start_time: this.start,
+            end_time: this.end
           }
         });
         if (!res) return;
@@ -520,7 +538,13 @@ export default {
     },
     // 活动时段:meter 柱状图,系列[0]为背景轨道,系列[1]为各时段数量
     buildRhythmChart(periodList) {
-      if (!periodList.length) return;
+      if (!periodList.length) {
+        // 无数据时清空旧图,显示空态(不再保留上一次的数据)
+        this.rhythmEmpty = true;
+        this.rhythmChart = null;
+        return;
+      }
+      this.rhythmEmpty = false;
       const categories = [];
       const totals = [];
       let maxTotal = 0;
@@ -555,6 +579,8 @@ export default {
     // 鼠害占比:环形图 + 列表,物种来自 target_detail
     buildCommunity(total, detail) {
       const names = Object.keys(detail);
+      // 无数据时显示空态(不再保留上一次的数据)
+      this.communityEmpty = !names.length || !total;
       const list = names.map((name, index) => {
         const count = detail[name] || 0;
         return {
@@ -570,7 +596,8 @@ export default {
       this.communityOpts.tooltip = { index: -1 };
       this.communityChart = {
         series: [{
-          data: list.map(item => ({ name: item.name, value: item.count }))
+          // 每项带上与列表圆点一致的 color,保证环形扇区与下方列表颜色一一对应
+          data: list.map(item => ({ name: item.name, value: item.count, color: item.color }))
         }]
       };
       // 环形中心显示占比最高的物种
@@ -728,6 +755,16 @@ export default {
 .chart-wrap--trend {
   height: 420rpx;
 }
+// 图表无数据空态
+.chart-empty {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 240rpx;
+  font-size: 26rpx;
+  color: #999999;
+  font-family: 'Source Han Sans CN VF';
+}
 // 群落结构:上下布局,环形图在上,图例列表在下
 .community-content {
   display: flex;

+ 23 - 3
pages/shuHai/detail.vue

@@ -89,7 +89,7 @@
           :day="day"
           :pest="pest"
         />
-        <RodentAnalysis :active="activeTab === 'pestAnalysis'" :device-id="deviceInfo.id" />
+        <RodentAnalysis :active="activeTab === 'pestAnalysis'" :device-id="deviceInfo.id" :start="analysisStart" :end="analysisEnd" />
         <!-- <PestArchive
           :pest_info="pestInfo"
         /> -->
@@ -223,6 +223,15 @@ export default {
       this.selectorRange.push(item);
     }
   },
+  computed: {
+    // 鼠情分析查询范围(秒级时间戳)
+    analysisStart() {
+      return this.startDate ? new Date(this.startDate + ' 00:00:00').getTime() / 1000 : 0;
+    },
+    analysisEnd() {
+      return this.endDate ? new Date(this.endDate + ' 23:59:59').getTime() / 1000 : 0;
+    }
+  },
   methods: {
     formatePestMessage(data) {
       if (data.length === 0) {
@@ -354,6 +363,10 @@ export default {
         this.maxDate = this.formatDate(new Date(this.currentYear, 11, 31));
         this.minDate = this.formatDate(new Date(this.currentYear, 0, 1));
       }
+      // 切换年份后重置分页并清空旧的事件列表
+      this.page = 1;
+      this.imageList = [];
+      this.imgTotal = 0;
       this.initAction();
       this.showPicker = false;
     },
@@ -371,6 +384,9 @@ export default {
       this.startDate = e.startDate;
       this.endDate = e.endDate;
       this.page = 1;
+      // 切换日期后先清空旧的事件列表,避免接口异常或返回空时残留上一时间段的数据
+      this.imageList = [];
+      this.imgTotal = 0;
       this.initAction();
     },
     showCalendar(){
@@ -679,7 +695,8 @@ export default {
     .tabs-timer-container{
       display: flex;
       align-items: center;
-      width: calc(100% - 256rpx);
+      justify-content: center;
+      width: calc(100% - 206rpx);
       height: 64rpx;
       line-height: 64rpx;
       text-align: center;
@@ -692,8 +709,10 @@ export default {
       color: #656565;
       margin: 16rpx;
       position: relative;
+      white-space: nowrap;
       .tabs-timer-item{
-        width: 42%;
+        width: auto;
+        flex-shrink: 0;
         color: #656565;
         text-align: center;
         font-family: "Source Han Sans CN VF";
@@ -702,6 +721,7 @@ export default {
       }
       .calendar-icon{
         margin-left: 24rpx;
+        flex-shrink: 0;
       }
     }
     .tab-item {

+ 8 - 8
pages/shuHai/deviceControl.vue

@@ -127,9 +127,9 @@
             <view class="device-detail-child-settings" v-if="item.activeTab === 'schedule'">
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">定时拍照:</text>
-                <text class="device-detail-value">
+                <view class="device-detail-value">
                   <u-switch v-model="item.timeSettings.timePhoto" size="36" />
-                </text>
+                </view>
               </view>
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">循环拍摄间隔:</text>
@@ -173,9 +173,9 @@
             <view class="device-detail-child-settings" v-if="item.activeTab === 'warning'">
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">PIR开关:</text>
-                <text class="device-detail-value">
+                <view class="device-detail-value">
                   <u-switch v-model="item.warning.pirOpenOrClose" size="36" />
-                </text>
+                </view>
               </view>
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">PIR灵敏度:</text>
@@ -195,15 +195,15 @@
             <view class="device-detail-child-settings" v-if="item.activeTab === 'other'">
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">信息戳记:</text>
-                <text class="device-detail-value">
+                <view class="device-detail-value">
                   <u-switch v-model="item.other.informationStamp" size="36" />
-                </text>
+                </view>
               </view>
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">自动覆盖:</text>
-                <text class="device-detail-value">
+                <view class="device-detail-value">
                   <u-switch v-model="item.other.autoOverWrite" size="36" />
-                </text>
+                </view>
               </view>
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">相机名称:</text>

+ 59 - 2
pages/shuHai/devicePhoto.vue

@@ -45,11 +45,16 @@
     <view class="main-photo" v-if="currentImg.addr">
       <movable-area class="movable-area" :style="{ height: containerHeight }">
         <movable-view
+          :key="currentImg.addr"
           class="movable-view"
           direction="all"
           scale="true"
           scale-min="1"
           scale-max="5"
+          :scale-value="movableScale"
+          :x="movableX"
+          :y="movableY"
+          @tap="onViewerTap"
         >
           <view class="image-container">
             <!-- 原图 -->
@@ -224,6 +229,11 @@ export default {
       is_jpg:1,
       imgOld_id:'',
       containerHeight: '300px',
+      // 主图缩放/位移受控值:双击复位 & 切图重置用(movable-view 缩放拖动后易视图卡住拖不回)
+      movableX: 0,
+      movableY: 0,
+      movableScale: 1,
+      lastTapTime: 0,
       pestDict: {}, // 虫子字典 {id: name}
       pestColorMap: {}, // 害虫颜色映射 {pestName: color}
     };
@@ -298,16 +308,41 @@ export default {
         this.defaultDate = this.minDate;
       }
       this.showPicker = false;
-      this.pestList = [];
+      // 切换年份后先清空上一组数据,再按新年份重新拉取
+      this.fileList = [];
       this.thumbnails = [];
+      this.pestList = [];
+      this.currentImg = {};
+      this.event_id = '';
+      this.camera_id = '';
+      await this.getEventList();
+      this.fileIndex = 0;
+      const curFile = this.fileList[0];
+      if (curFile) {
+        this.event_id = curFile.event_id;
+        this.camera_id = curFile.device_id;
+      }
       await this.getPestList();
     },
     async handleChange(e){
       this.time_begin = e.result;
       this.time_end = e.result;
+      // 切换日期后先清空上一组数据(事件列表/缩略图/识别结果),再按新日期重新拉取
+      this.fileList = [];
+      this.thumbnails = [];
       this.pestList = [];
-      await this.getPestList()
       this.currentImg = {};
+      this.event_id = '';
+      this.camera_id = '';
+      await this.getEventList();
+      // 默认选中新日期下的第一个事件
+      this.fileIndex = 0;
+      const curFile = this.fileList[0];
+      if (curFile) {
+        this.event_id = curFile.event_id;
+        this.camera_id = curFile.device_id;
+      }
+      await this.getPestList();
     },
     getPestColor(pestName) {
       // 如果该害虫已有颜色,直接返回
@@ -376,6 +411,11 @@ export default {
       });
       this.thumbnails = res?.data || [];
       this.currentImg = {};
+      if (!this.thumbnails.length) {
+        // 无数据时不再请求详情
+        this.img_id = '';
+        return;
+      }
       //如果和旧的id相同,就用旧的id不然就用第一个
       const oldImg = this.thumbnails.find(item => item.id == this.imgOld_id);
       this.img_id = oldImg?.id || this.thumbnails[0]?.id || ''
@@ -623,8 +663,23 @@ export default {
       uni.navigateBack();
     },
     selectThumbnail(item) {
+      this.resetViewer();
       this.img_id = item.id;
       this.getDevicePhotoDetails();
+    },
+    // 双击主图复位缩放与位置:movable-view 缩放拖动后可能视图卡住、拖不回来
+    onViewerTap() {
+      const now = Date.now();
+      if (now - this.lastTapTime < 300) {
+        this.resetViewer();
+      }
+      this.lastTapTime = now;
+    },
+    resetViewer() {
+      this.movableScale = 1;
+      // 交替设一个极小值,保证属性变化能触发 movable-view 复位
+      this.movableX = this.movableX === 0 ? 0.01 : 0;
+      this.movableY = this.movableY === 0 ? 0.01 : 0;
     }
   }
 };
@@ -816,6 +871,8 @@ export default {
 
     .movable-view {
       width: 100%;
+      // 显式高度:movable-view 依赖宽高计算各方向拖动边界,高度缺失会导致放大后无法上下拖动
+      height: 100%;
     }
 
     .image-container {

+ 2 - 2
pages/shuHai1/components/deviceData.vue

@@ -165,9 +165,9 @@
             <text class="sim-value"><text style="color: #0BBC58">{{ deviceInfoStatic.used || ''}}</text>/{{ deviceInfoStatic.total || '' }}</text>
           </view>
           <view class="sim-item">
-            <text class="sim-value">
+            <view class="sim-value">
               <u-line-progress active-color="#0BBC58" :percent="deviceInfoStatic.used / deviceInfoStatic.total * 100" :show-percent="false"></u-line-progress>
-            </text>
+            </view>
           </view>
         </view>
         <view class="sim-container" v-if="activeTab === 1">

+ 129 - 0
pages/shuHai1/components/downImage.vue

@@ -0,0 +1,129 @@
+<template>
+  <view class="down-image">
+    <view class="down-image__content" v-if="images.length">
+      <view class="down-image__grid">
+        <view class="down-image__item" v-for="(item, index) in images" :key="item.id || index" @click="handleClick(index)">
+          <view class="down-image__item-img-container">
+            <image
+              class="down-image__item-img"
+              :src="item.img"
+              mode="aspectFill"
+              lazy-load
+            />
+          </view>
+          <view class="down-image__item-title">
+            {{ formatDate(item.uptime) }}
+          </view>
+        </view>
+      </view>
+    </view>
+    <view v-else>
+      <u-empty text="暂无图片"></u-empty>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  props:{
+    images:{
+      type: Array,
+      default: () => []
+    },
+    deviceInfo:{
+      type: Object,
+      default: () => ({})
+    },
+  },
+  methods: {
+    // 点击跳转查看图片页,与监测图像(前后左右)一致
+    handleClick(index) {
+      const item = this.images[index] || {};
+      // 只带当前点击的这张图片,URL放不下长链接,走storage
+      const imgs = [{
+        addr: item.img,
+        mark: this.parseMark(item.mark),
+        addtime: item.uptime,
+      }];
+      uni.setStorageSync('shuhai_group_imgs', JSON.stringify(imgs));
+      uni.navigateTo({
+        url: '/pages/shuHai1/devicePhoto?addtime=' + (item.uptime || 0) + '&id=' + (this.deviceInfo.id || '') + '&group_id=' + (item.id || '') + '&photo_type=' + encodeURIComponent('下')
+      })
+    },
+    // mark可能是JSON字符串
+    parseMark(mark) {
+      if (Array.isArray(mark)) {
+        return mark;
+      }
+      try {
+        const arr = JSON.parse(mark || '[]');
+        return Array.isArray(arr) ? arr : [];
+      } catch (error) {
+        return [];
+      }
+    },
+    // 格式化时间
+    formatDate(dateString) {
+      if(!dateString){
+        return '';
+      }
+      const date = new Date(dateString*1000);
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, '0');
+      const day = String(date.getDate()).padStart(2, '0');
+      const hour = String(date.getHours()).padStart(2, '0');
+      const minute = String(date.getMinutes()).padStart(2, '0');
+      const second = String(date.getSeconds()).padStart(2, '0');
+      return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.down-image {
+  width: 100%;
+  box-sizing: border-box;
+
+  .down-image__content{
+    margin-top: 24rpx;
+    .down-image__grid{
+      display: grid;
+      grid-template-columns: repeat(2, 1fr);
+      gap: 20rpx;
+    }
+  }
+  &__item {
+    border-radius: 16rpx;
+    padding: 20rpx;
+    position: relative;
+    width: 100%;
+    box-sizing: border-box;
+    display: inline-block;
+    overflow: hidden;
+    background: #ffffff;
+    &-img-container {
+      width: 100%;
+      padding-bottom: 60%;
+      position: relative;
+      border-radius: 16rpx;
+      overflow: hidden;
+      background-color: #f5f5f5;
+    }
+    &-img {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+    }
+    &-title {
+      font-size: 28rpx;
+      color: #333333;
+      font-family: "Source Han Sans CN VF";
+      font-weight: 500;
+      margin-top: 12rpx;
+    }
+  }
+}
+</style>

+ 1 - 0
pages/shuHai1/components/pestDiscern.vue

@@ -76,6 +76,7 @@ export default {
   box-sizing: border-box;
   background: #fff;
   border-radius: 16rpx;
+  margin-bottom: 26rpx;
 }
 .pest-discern__header {
   padding: 0rpx 0rpx 20rpx 0rpx;

+ 47 - 16
pages/shuHai1/components/rodentAnalysis.vue

@@ -22,8 +22,8 @@
           >年</view>
         </view> -->
       </view>
-      <view class="trend-legend">
-        <view class="trend-legend__item" v-for="item in trendLegend" :key="item.name">
+      <view class="trend-legend" v-if="trendLegendList.length">
+        <view class="trend-legend__item" v-for="item in trendLegendList" :key="item.name">
           <view class="trend-legend__line" :style="{ background: item.color }">
             <view class="trend-legend__dot" :style="{ borderColor: item.color }"></view>
           </view>
@@ -31,7 +31,7 @@
         </view>
       </view>
       <view class="trend-axis-label">鼠害数量:只</view>
-      <view class="chart-wrap chart-wrap--trend">
+      <view class="chart-wrap chart-wrap--trend" v-if="!trendEmpty">
         <qiun-data-charts
           type="line"
           :chartData="trendChart"
@@ -42,6 +42,7 @@
           :ontouch="true"
         />
       </view>
+      <view v-else class="chart-empty">暂无数据</view>
     </view>
 
     <!-- 鼠害占比 -->
@@ -50,7 +51,7 @@
         <view class="analysis-card__title">鼠害占比</view>
       </view>
       <view class="community-content">
-        <view class="chart-wrap chart-wrap--ring">
+        <view class="chart-wrap chart-wrap--ring" v-if="communityList.length">
           <qiun-data-charts
             type="ring"
             :chartData="communityChart"
@@ -63,6 +64,7 @@
             @getIndex="onRingIndex"
           />
         </view>
+        <view v-else class="chart-empty">暂无数据</view>
         <view class="community-total">
           <text>总鼠害数量</text>
           <text class="total-value">{{ totalCount }}
@@ -121,6 +123,12 @@ export default {
     return {
       // 发生趋势
       trendChart: null,
+      // 无数据空态:为 true 时不渲染折线图(空 series 会让 qiun-data-charts 一直转 loading)
+      trendEmpty: false,
+      // 趋势图图例自存一份数据,不读 trendChart.series,保证与分配的颜色一致
+      trendLegendList: [],
+      // 物种名 → 颜色:同名物种固定同色,日期范围/系列顺序变化时两图颜色不跳变
+      nameColorMap: {},
       trendOpts: {
         // 颜色顺序与 series 一一对应
         color: ['#FF5252', '#0BBC58', '#3D5AFE'],
@@ -204,11 +212,6 @@ export default {
     };
   },
   computed: {
-    // 趋势图图例:跟随接口返回的物种系列
-    trendLegend() {
-      if (!this.trendChart) return [];
-      return this.trendChart.series.map(s => ({ name: s.name, color: s.color }));
-    },
     // 依赖项拼接,任一变化即重新拉取
     fetchKey() {
       return [this.deviceId, this.start, this.end].join('_');
@@ -250,13 +253,28 @@ export default {
     // 发生趋势:x 轴为日期(MM-DD),系列为接口返回的各物种
     buildTrendChart(categories, seriesList) {
       const cats = categories.map(date => (date || '').slice(5));
-      const series = seriesList.map((item, index) => ({
-        name: item.name,
-        color: TREND_PALETTE[index % TREND_PALETTE.length],
-        data: (item.data || []).map(v => v || 0)
-      }));
+      // 无数据走空态,避免折线图一直loading
+      this.trendEmpty = !cats.length || !seriesList.length;
+      // 同名物种固定同色;新物种从色板里挑本次未用过的颜色,避免同一张图两个系列撞色
+      const usedColors = [];
+      const series = seriesList.map((item) => {
+        let color = this.nameColorMap[item.name];
+        if (!color) {
+          color = TREND_PALETTE.find(c => !usedColors.includes(c)) || TREND_PALETTE[usedColors.length % TREND_PALETTE.length];
+          this.nameColorMap[item.name] = color;
+        }
+        usedColors.push(color);
+        return {
+          name: item.name,
+          color,
+          data: (item.data || []).map(v => v || 0)
+        };
+      });
+      this.trendLegendList = series.map(s => ({ name: s.name, color: s.color }));
       this.trendChart = { categories: cats, series };
-      if (!cats.length) return;
+      // 引擎色板与系列一一对应,保证线条渲染颜色与图例/占比图一致
+      this.trendOpts.color = series.map(s => s.color);
+      if (this.trendEmpty) return;
       // Y 轴上限随数据量调整,向上取整到 10 的倍数
       let maxVal = 0;
       series.forEach(s => {
@@ -295,11 +313,14 @@ export default {
           data: list.map(item => ({ name: item.name, value: item.count }))
         }]
       };
-      // 环形中心显示占比最高的物种
+      // 环形中心显示占比最高的物种;无数据时清掉上次查询残留的中心文字
       if (list.length) {
         const top = list.reduce((a, b) => (b.count > a.count ? b : a), list[0]);
         this.communityOpts.title.name = top.percent + '%';
         this.communityOpts.subtitle.name = top.name;
+      } else {
+        this.communityOpts.title.name = '0%';
+        this.communityOpts.subtitle.name = '--';
       }
     },
     // 点击占比列表:环形图高亮对应扇区,中心数据联动;再次点击取消选中
@@ -450,6 +471,16 @@ export default {
 .chart-wrap--trend {
   height: 420rpx;
 }
+// 图表无数据空态
+.chart-empty {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 240rpx;
+  font-size: 26rpx;
+  color: #999999;
+  font-family: 'Source Han Sans CN VF';
+}
 // 群落结构:上下布局,环形图在上,图例列表在下
 .community-content {
   display: flex;

+ 167 - 91
pages/shuHai1/detail.vue

@@ -13,18 +13,18 @@
     <view class="operation-container" v-if="isShowOperation" @click="closeOperationHandler">
       <view class="operation-background"></view>
       <view class="operation-content">
-        <view class="operation-item" v-if="isShowPhoto" @click="handlePhotoClick">
+        <!-- <view class="operation-item" v-if="isShowPhoto" @click="handlePhotoClick">
           <image :src="photoIcon" class="operation-icon"></image>
           拍照
-        </view>
+        </view> -->
         <view class="operation-item" @click="handleServiceClick">
           <image :src="serviceIcon" class="operation-icon"></image>
           维修
         </view>
-        <view class="operation-item" @click="handleSettingClick">
+        <!-- <view class="operation-item" @click="handleSettingClick">
           <image :src="settingIcon" class="operation-icon"></image>
           设置
-        </view>
+        </view> -->
         <!-- <view class="operation-item" @click="handleSimClick">
           <image :src="simIcon" class="operation-icon"></image>
           SIM卡
@@ -65,13 +65,20 @@
             </view>
           </view>
           <view class="tabs-timer-container" @click="showCalendar">
-            <view class="tabs-timer-item">
-              {{startDate}}
-            </view>
-            至
-            <view class="tabs-timer-item">
-              {{endDate}}
-            </view>
+            <block v-if="isRangeDate">
+              <view class="tabs-timer-item">
+                {{startDate}}
+              </view>
+              至
+              <view class="tabs-timer-item">
+                {{endDate}}
+              </view>
+            </block>
+            <block v-else>
+              <view class="tabs-timer-item">
+                {{imageDate}}
+              </view>
+            </block>
             <u-icon name="calendar" size="36" class="calendar-icon"></u-icon>
           </view>
         </view>
@@ -81,14 +88,6 @@
           :total="total"
           :pest_order="pest_order"
         />
-        <PestEchart
-          :pest_order="pest_order"
-          @getInfo="getInfo" 
-          :endDate="endDate"
-          :d_id="deviceInfo.d_id"
-          :day="day"
-          :pest="pest"
-        />
         <RodentAnalysis :active="activeTab === 'pestAnalysis'" :device-id="deviceInfo.id" :start="analysisStart" :end="analysisEnd" />
         <!-- <PestArchive
           :pest_info="pestInfo"
@@ -97,21 +96,36 @@
           <u-empty text="暂无数据"></u-empty>
         </view> -->
       </view>
-      <view v-if="activeTab === 'viewImage'">
-        <!-- 实现下拉加载 -->
-         <scroll-view class="scroll-view" scroll-y style="height: 60vh;" @scrolltolower="handleReachBottom">
-          <photoImage
-            :images="imageList"
-            :pestList="pestList"
-            :disableShow="disableShow"
-            :ding_one_device_id="ding_one_device_id"
-            :ding_three_device_id="ding_three_device_id"
-            :ding_two_device_id="ding_two_device_id"
-            @changeTab="changeTab"
-            :currentYear="currentYear"
-            :deviceInfo="deviceInfo"
-          />
-        </scroll-view>
+      <view class="tabs" v-if="activeTab === 'viewImage'">
+        <view class="tab-container">
+          <view class="tab-item" :class="imageTab === 'viewImage'?'active':''" @click="handleImageTabClick('viewImage')">
+            监测图像(前后左右)
+          </view>
+          <view class="tab-item" :class="imageTab === 'downImage'?'active':''" @click="handleImageTabClick('downImage')">
+            监测图像(下)
+          </view>
+        </view>
+        <view v-show="imageTab === 'viewImage'">
+          <!-- 实现下拉加载 -->
+           <scroll-view class="scroll-view" scroll-y style="height: 60vh;" @scrolltolower="handleReachBottom">
+            <photoImage
+              :images="imageList"
+              :pestList="pestList"
+              :disableShow="disableShow"
+              :ding_one_device_id="ding_one_device_id"
+              :ding_three_device_id="ding_three_device_id"
+              :ding_two_device_id="ding_two_device_id"
+              @changeTab="changeTab"
+              :currentYear="currentYear"
+              :deviceInfo="deviceInfo"
+            />
+          </scroll-view>
+        </view>
+        <view v-show="imageTab === 'downImage'">
+          <scroll-view class="scroll-view" scroll-y style="height: 60vh;">
+            <downImage :images="downImageList" :deviceInfo="deviceInfo" />
+          </scroll-view>
+        </view>
       </view>
       <view v-if="activeTab === 'aiAnalysis'">
         <AiChat
@@ -131,16 +145,16 @@
         />
       </view>
     </view>
-    <u-calendar v-model="show" :mode="mode" range-color="#999" btn-type="success" active-bg-color="#0BBC58" range-bg-color="rgba(11,188,88,0.13)" @change="handleChange" :max-date="maxDate" :min-date="minDate"></u-calendar>
+    <u-calendar v-model="show" :mode="calendarMode" range-color="#999" btn-type="success" active-bg-color="#0BBC58" range-bg-color="rgba(11,188,88,0.13)" @change="handleChange" :max-date="maxDate" :min-date="minDate"></u-calendar>
     <u-picker v-model="showPicker" mode="selector" :range="selectorRange" range-key="id" :default-selector="[0]" @confirm="confirmHandler"></u-picker>
   </view>
 </template>
 <script>
 import DeviceCard from './components/DeviceCard.vue';
 import PestDiscern from './components/pestDiscern.vue';
-import PestEchart from './components/pestEchart.vue';
 import PestArchive from './components/pestArchive.vue';
 import photoImage from './components/photoImage.vue';
+import downImage from './components/downImage.vue';
 import DeviceData from './components/deviceData.vue';
 import AiChat from './components/aiChat.vue';
 import RodentAnalysis from './components/rodentAnalysis.vue';
@@ -150,9 +164,9 @@ export default {
     DeviceCard,
     PestDiscern,
     AiChat,
-    PestEchart,
     PestArchive,
     photoImage,
+    downImage,
     DeviceData,
     RodentAnalysis,
   },
@@ -174,24 +188,25 @@ export default {
       minDate: this.formatDate(new Date(new Date().getFullYear(), 0, 1)),
       show: false,
       showSim: false,
-      // 当前日期向前推30天 格式2026-01-01 月份和日期小于10的时候前面加个0
-      startDate: this.formatDate(new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000)),
+      // 鼠情分析/设备数据用的时间区间:结束为今天,开始为7天前 格式2026-01-01
+      startDate: this.formatDate(new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000)),
       endDate: this.formatDate(new Date()),
-      mode: 'range',
+      // 图像数据用单日,默认今天(onLoad会按time_folder接口更新为最近有数据的日期)
+      imageDate: this.formatDate(new Date()),
       imageList: [],
+      downImageList: [],
       pest_order:{},
       pestMessage:'',
       deviceInfo: {},
-      pestInfo: {},
       time: '',
       activeTab: 'pestAnalysis',
+      // 图像数据内的子切换:viewImage-前后左右 downImage-下
+      imageTab: 'viewImage',
       deviceType: '',
       camera_id: '',
       location: '',
       total: 0,
       imgTotal: 0,
-      day: [],
-      pest: [],
       page:1,
       is_pest:'',
       page_size:24,
@@ -206,6 +221,13 @@ export default {
     }
   },
   computed: {
+    // 鼠情分析/设备数据用区间选择,图像数据用单日选择
+    isRangeDate() {
+      return this.activeTab === 'pestAnalysis' || this.activeTab === 'deviceData';
+    },
+    calendarMode() {
+      return this.isRangeDate ? 'range' : 'date';
+    },
     // 鼠情分析查询范围(秒级时间戳)
     analysisStart() {
       return this.startDate ? new Date(this.startDate + ' 00:00:00').getTime() / 1000 : 0;
@@ -214,12 +236,14 @@ export default {
       return this.endDate ? new Date(this.endDate + ' 23:59:59').getTime() / 1000 : 0;
     }
   },
-  onLoad(options){
+  async onLoad(options){
     this.deviceInfo = JSON.parse(options.info);
     const { ding_one_device_id, ding_three_device_id, ding_two_device_id } = this.deviceInfo;
     this.ding_one_device_id = ding_one_device_id;
     this.ding_three_device_id = ding_three_device_id;
     this.ding_two_device_id = ding_two_device_id;
+    // 先取有数据的日期,默认选中最近的一天,再加载各模块数据
+    await this.initDateFolder();
     this.getPestAnalysis();
     this.isShow();
     const currentYear = new Date().getFullYear();
@@ -346,29 +370,26 @@ export default {
     },
     confirmHandler(e){
       this.currentYear = this.selectorRange[e].id;
-      if(this.currentYear == new Date().getFullYear()){
-        // 结束日期为this.endDate的月份和日期加上选择的年份
+      if(this.calendarMode === 'range'){
+        // 区间选择:结束日期保持月日仅切换年份,开始日期为结束日期前7天
         const timeDate = this.currentYear + '-' + this.endDate.split('-')[1] + '-' + this.endDate.split('-')[2];
         this.endDate = this.formatDate(new Date(timeDate));
-        // 开始日期为结束日期前30天
-        this.startDate = this.formatDate(new Date(new Date(this.endDate).getTime() - 30 * 24 * 60 * 60 * 1000));
+        this.startDate = this.formatDate(new Date(new Date(this.endDate).getTime() - 7 * 24 * 60 * 60 * 1000));
+      }else{
+        // 单日选择:保持月日不变,仅切换年份
+        const timeDate = this.currentYear + '-' + this.imageDate.split('-')[1] + '-' + this.imageDate.split('-')[2];
+        this.imageDate = this.formatDate(new Date(timeDate));
+      }
+      if(this.currentYear == new Date().getFullYear()){
         this.maxDate = this.formatDate(new Date());
         this.minDate = this.formatDate(new Date(new Date().getFullYear(), 0, 1));
       }else{
-        // 结束日期为this.endDate的月份和日期加上选择的年份
-        const timeDate = this.currentYear + '-' + this.endDate.split('-')[1] + '-' + this.endDate.split('-')[2];
-        this.endDate = this.formatDate(new Date(timeDate));
-        // 开始日期为结束日期前30天
-        this.startDate = this.formatDate(new Date(new Date(this.endDate).getTime() - 30 * 24 * 60 * 60 * 1000));
         this.maxDate = this.formatDate(new Date(this.currentYear, 11, 31));
         this.minDate = this.formatDate(new Date(this.currentYear, 0, 1));
       }
       this.initAction();
       this.showPicker = false;
     },
-    getInfo(info){
-      this.pestInfo = info;
-    },
     // 格式化日期为YYYY-MM-DD格式,月份和日期小于10时前面加0
     formatDate(date) {
       const year = date.getFullYear();
@@ -377,8 +398,12 @@ export default {
       return `${year}-${month}-${day}`;
     },
     handleChange(e){
-      this.startDate = e.startDate;
-      this.endDate = e.endDate;
+      if(this.calendarMode === 'range'){
+        this.startDate = e.startDate;
+        this.endDate = e.endDate;
+      }else{
+        this.imageDate = e.result;
+      }
       this.page = 1;
       this.initAction();
     },
@@ -397,8 +422,12 @@ export default {
       }else if(this.activeTab === 'aiAnalysis'){
         this.getAiAnalysis();
       }else if(this.activeTab === 'viewImage'){
-        this.initPest();
-        this.initImageList();
+        if(this.imageTab === 'viewImage'){
+          this.initPest();
+          this.initImageList();
+        }else{
+          this.initDownImageList();
+        }
       }else if(this.activeTab === 'deviceData'){
         this.getPolylineData();
         this.getDeviceHistoryData();
@@ -411,6 +440,14 @@ export default {
       this.activeTab = tab;
       this.initAction();
     },
+    // 图像数据内的子切换,不影响上方 图像数据/鼠情分析 的选中状态
+    handleImageTabClick(tab) {
+      if(this.imageTab === tab){
+        return
+      }
+      this.imageTab = tab;
+      this.initAction();
+    },
     async initImageList(isLoadMore){
       if(isLoadMore){
         this.page++;
@@ -423,8 +460,8 @@ export default {
         method: 'POST',
         data: {
           device_id: this.deviceInfo.id,
-          start: new Date(this.startDate + ' 00:00:00').getTime()/1000,// 秒级时间戳
-          end: new Date(this.endDate + ' 23:59:59').getTime()/1000,// 秒级时间戳
+          start: new Date(this.imageDate + ' 00:00:00').getTime()/1000,// 秒级时间戳
+          end: new Date(this.imageDate + ' 23:59:59').getTime()/1000,// 秒级时间戳
           page: this.page,
           page_number: this.page_size,
         },
@@ -438,6 +475,53 @@ export default {
         this.imageList = data;
       }
     },
+    async initDownImageList(){
+      uni.showLoading({
+        title: '加载中',
+      })
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=forecast.shuhai.shuhai_down_imgs',
+        method: 'POST',
+        data: {
+          device_id: this.deviceInfo.id,
+          start: new Date(this.imageDate + ' 00:00:00').getTime()/1000,// 秒级时间戳
+          end: new Date(this.imageDate + ' 23:59:59').getTime()/1000,// 秒级时间戳
+        },
+      });
+      uni.hideLoading();
+      this.downImageList = res?.data || [];
+    },
+    // 获取有数据的日期,默认选中最近的一天
+    async initDateFolder(){
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=forecast.shuhai.shuhai_time_folder',
+        method: 'POST',
+        data: {
+          device_id: this.deviceInfo.id,
+        },
+      });
+      // 返回结构:[{ "2026": { "01": [3] } }, { "2025": { "12": [13], ... } }]
+      const folders = res?.data || [];
+      const dates = [];
+      folders.forEach(yearItem => {
+        for(const year in yearItem){
+          const months = yearItem[year] || {};
+          for(const month in months){
+            (months[month] || []).forEach(day => {
+              dates.push(`${year}-${month}-${String(day).padStart(2, '0')}`);
+            });
+          }
+        }
+      });
+      if(!dates.length){
+        return;
+      }
+      // YYYY-MM-DD 格式可直接按字符串比较
+      dates.sort((a, b) => b.localeCompare(a));
+      const today = this.formatDate(new Date());
+      // 日历最大可选今天,数据日期超过今天时取今天
+      this.imageDate = dates[0] > today ? today : dates[0];
+    },
     async initPest(){
       const res = await this.$myRequest({
         url: '/api/api_gateway?method=forecast.new_cbd.pest_type_list',
@@ -447,8 +531,8 @@ export default {
           page_size:999999,
           device_id: this.deviceInfo.id,
           identify_model: 'B',
-          time_begin: this.formatDate(new Date(this.startDate)) + ' 00:00:00',// 格式化开始时间YYYY-MM-DD HH:MM:SS
-          time_end: this.formatDate(new Date(this.endDate)) + ' 23:59:59',// 格式化结束时间YYYY-MM-DD HH:MM:SS
+          time_begin: this.formatDate(new Date(this.imageDate)) + ' 00:00:00',// 格式化开始时间YYYY-MM-DD HH:MM:SS
+          time_end: this.formatDate(new Date(this.imageDate)) + ' 23:59:59',// 格式化结束时间YYYY-MM-DD HH:MM:SS
         },
       });
       const data = res?.data || [];
@@ -490,42 +574,30 @@ export default {
     },
     async getPestAnalysis(){
       const res = await this.$myRequest({
-        url: '/api/api_gateway?method=forecast.cbd_analysis.analysis_pest_result',
+        url: '/api/api_gateway?method=forecast.shuhai.shuhai_analysis',
         method: 'POST',
         data: {
-        d_id: this.deviceInfo.d_id,
-        start: this.startDate,
-        end: this.endDate,
-        model: 'B'
+        device_id: this.deviceInfo.id,
+        start: this.analysisStart,// 秒级时间戳
+        end: this.analysisEnd,// 秒级时间戳
         },
       });
-      console.log(res,'resresres')
-      const pest_order = res?.pest_order;
-      this.getInfo({});
-      let total = 0;
+      // total:[{name,value}] → pest_order:{name:value},供识别统计/物种图表tabs使用
+      const totalList = res?.total || [];
+      const pest_order = {};
+      totalList.forEach(item => {
+        pest_order[item.name] = item.value || 0;
+      });
+      const total = totalList.reduce((sum, item) => sum + (item.value || 0), 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
+          percent: total ? (pest_order[key] / total) * 100 : 0
         })
       }
       this.pest_order = pest_order;
-      this.formatePestMessage([...data.reverse()]);
-      this.day = res?.day || [];
-      const pest = res?.pest || [];
-      this.pest = pest;
-      // pest.forEach(p =>{
-      //   for(let i = 0;i< p.length;i++){
-      //     this.pest.push(p[i]);
-      //   }
-      // })
+      this.formatePestMessage([...totalList].reverse());
       this.total = total;
     }
   }
@@ -687,7 +759,8 @@ export default {
     .tabs-timer-container{
       display: flex;
       align-items: center;
-      width: calc(100% - 256rpx);
+      justify-content: center;
+      width: calc(100% - 206rpx);
       height: 64rpx;
       line-height: 64rpx;
       text-align: center;
@@ -700,8 +773,10 @@ export default {
       color: #656565;
       margin: 16rpx;
       position: relative;
+      white-space: nowrap;
       .tabs-timer-item{
-        width: 42%;
+        width: auto;
+        flex-shrink: 0;
         color: #656565;
         text-align: center;
         font-family: "Source Han Sans CN VF";
@@ -710,6 +785,7 @@ export default {
       }
       .calendar-icon{
         margin-left: 24rpx;
+        flex-shrink: 0;
       }
     }
     .tab-item {

+ 8 - 8
pages/shuHai1/deviceControl.vue

@@ -127,9 +127,9 @@
             <view class="device-detail-child-settings" v-if="item.activeTab === 'schedule'">
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">定时拍照:</text>
-                <text class="device-detail-value">
+                <view class="device-detail-value">
                   <u-switch v-model="item.timeSettings.timePhoto" size="36" />
-                </text>
+                </view>
               </view>
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">循环拍摄间隔:</text>
@@ -173,9 +173,9 @@
             <view class="device-detail-child-settings" v-if="item.activeTab === 'warning'">
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">PIR开关:</text>
-                <text class="device-detail-value">
+                <view class="device-detail-value">
                   <u-switch v-model="item.warning.pirOpenOrClose" size="36" />
-                </text>
+                </view>
               </view>
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">PIR灵敏度:</text>
@@ -195,15 +195,15 @@
             <view class="device-detail-child-settings" v-if="item.activeTab === 'other'">
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">信息戳记:</text>
-                <text class="device-detail-value">
+                <view class="device-detail-value">
                   <u-switch v-model="item.other.informationStamp" size="36" />
-                </text>
+                </view>
               </view>
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">自动覆盖:</text>
-                <text class="device-detail-value">
+                <view class="device-detail-value">
                   <u-switch v-model="item.other.autoOverWrite" size="36" />
-                </text>
+                </view>
               </view>
               <view class="device-detail-viewImage">
                 <text class="device-detail-label">相机名称:</text>

+ 52 - 2
pages/shuHai1/devicePhoto.vue

@@ -15,11 +15,16 @@
     <view class="main-photo" v-if="currentImg.addr">
       <movable-area class="movable-area" :style="{ height: containerHeight }">
         <movable-view
+          :key="currentIndex"
           class="movable-view"
           direction="all"
           scale="true"
           scale-min="1"
           scale-max="5"
+          :scale-value="movableScale"
+          :x="movableX"
+          :y="movableY"
+          @tap="onViewerTap"
         >
           <view class="image-container">
             <image
@@ -50,7 +55,9 @@
           </view>
         </movable-view>
       </movable-area>
-      <view class="photo-timestamp">{{ formatDate(addtime) }}</view>
+      <!-- 图片来源角标:监测图像(下)显示"下",前后左右显示type(1/2/3/4) -->
+      <view class="photo-type-badge" v-if="badgeText">{{ badgeText }}</view>
+      <view class="photo-timestamp">{{ formatDate(currentImg.addtime || addtime) }}</view>
     </view>
     <view class="main-photo-empty" v-else>
       <u-empty text="暂无图片"></u-empty>
@@ -102,6 +109,8 @@ export default {
       title: '查看图片',
       addtime: '',
       group_id: '',
+      // 图片来源角标:从监测图像(下)进入时为"下",从前后左右进入时为空(用图片自身type)
+      photoType: '',
       // 该分组的所有图片
       thumbnails: [],
       currentIndex: 0,
@@ -109,6 +118,11 @@ export default {
       imageWidth: 0,
       imageHeight: 0,
       containerHeight: '300px',
+      // 主图缩放/位移受控值:双击复位 & 切图重置用(movable-view 缩放拖动后易视图卡住拖不回)
+      movableX: 0,
+      movableY: 0,
+      movableScale: 1,
+      lastTapTime: 0,
       pestList: [],
       colors: [
         '#FF5951',
@@ -129,11 +143,16 @@ export default {
     currentImg() {
       return this.thumbnails[this.currentIndex] || {};
     },
+    // 角标内容:优先用页面带过来的"下",否则用当前图片的type(1/2/3/4)
+    badgeText() {
+      return this.photoType || this.currentImg.type || '';
+    },
   },
   onLoad(options) {
     const { addtime, group_id } = options;
     this.addtime = Number(addtime) || 0;
     this.group_id = group_id || '';
+    this.photoType = options.photo_type ? decodeURIComponent(options.photo_type) : '';
     // 取出列表页带过来的该分组全部图片
     try {
       const imgs = JSON.parse(uni.getStorageSync('shuhai_group_imgs') || '[]');
@@ -142,7 +161,9 @@ export default {
       this.thumbnails = [];
     }
     uni.removeStorageSync('shuhai_group_imgs');
-    this.currentIndex = 0;
+    // 初始定位到点击的那张(监测图像(下)列表会带index)
+    const index = Number(options.index) || 0;
+    this.currentIndex = Math.min(Math.max(index, 0), Math.max(this.thumbnails.length - 1, 0));
     this.processBugMarkers();
     this.buildPestList();
   },
@@ -152,9 +173,24 @@ export default {
         return;
       }
       this.currentIndex = index;
+      this.resetViewer();
       this.processBugMarkers();
       this.buildPestList();
     },
+    // 双击主图复位缩放与位置:movable-view 缩放拖动后可能视图卡住、拖不回来
+    onViewerTap() {
+      const now = Date.now();
+      if (now - this.lastTapTime < 300) {
+        this.resetViewer();
+      }
+      this.lastTapTime = now;
+    },
+    resetViewer() {
+      this.movableScale = 1;
+      // 交替设一个极小值,保证属性变化能触发 movable-view 复位
+      this.movableX = this.movableX === 0 ? 0.01 : 0;
+      this.movableY = this.movableY === 0 ? 0.01 : 0;
+    },
     // 根据当前图片的mark生成识别结果列表
     buildPestList() {
       const marks = this.currentImg.mark || [];
@@ -312,6 +348,8 @@ export default {
     }
     .movable-view {
       width: 100%;
+      // 显式高度:movable-view 依赖宽高计算各方向拖动边界,高度缺失会导致放大后无法上下拖动
+      height: 100%;
     }
     .image-container {
       position: relative;
@@ -359,6 +397,18 @@ export default {
       padding: 12rpx 16rpx;
       font-size: 26rpx;
     }
+    /* 图片来源角标 */
+    .photo-type-badge {
+      position: absolute;
+      top: 16rpx;
+      right: 16rpx;
+      z-index: 10;
+      background-color: rgba(0, 0, 0, 0.6);
+      color: #FFFFFF;
+      padding: 6rpx 16rpx;
+      border-radius: 8rpx;
+      font-size: 26rpx;
+    }
   }
   .main-photo-empty {
     margin: 24rpx;

+ 2 - 2
pages/sy/components/deviceData.vue

@@ -204,9 +204,9 @@
             <text class="sim-value"><text style="color: #0BBC58">{{ deviceInfoStatic.used || ''}}</text>/{{ deviceInfoStatic.total || '' }}</text>
           </view>
           <view class="sim-item">
-            <text class="sim-value">
+            <view class="sim-value">
               <u-line-progress active-color="#0BBC58" :percent="deviceInfoStatic.used / deviceInfoStatic.total * 100" :show-percent="false"></u-line-progress>
-            </text>
+            </view>
           </view>
         </view>
         <view class="sim-container" v-if="activeTab === 1">

+ 2 - 2
pages/sy/components/deviceData2.vue

@@ -81,9 +81,9 @@
             <text class="sim-value"><text style="color: #0BBC58">{{ deviceInfoStatic.used || ''}}</text>/{{ deviceInfoStatic.total || '' }}</text>
           </view>
           <view class="sim-item">
-            <text class="sim-value">
+            <view class="sim-value">
               <u-line-progress active-color="#0BBC58" :percent="deviceInfoStatic.used / deviceInfoStatic.total * 100" :show-percent="false"></u-line-progress>
-            </text>
+            </view>
           </view>
         </view>
         <view class="sim-container" v-if="activeTab === 1">

+ 9 - 3
pages/sy/detail.vue

@@ -1,6 +1,8 @@
 <template>
   <view class="device-detail">
-    <view class="device-detail__header">
+    <!-- 固定头部:滚动时内容不再钻到系统状态栏下面 -->
+    <nav-top-bar background="linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%)">
+      <view class="device-detail__header">
       <u-icon
         size="36"
         class="arrow-left"
@@ -8,7 +10,8 @@
         @click="handleBack"
       ></u-icon>
       {{ deviceInfo.name }}
-    </view>
+      </view>
+    </nav-top-bar>
     <view class="operation" @click.stop="isShowOperation = !isShowOperation">操作</view>
     <view class="operation-container" v-if="isShowOperation" @click="closeOperationHandler">
       <view class="operation-background"></view>
@@ -137,6 +140,7 @@
   </view>
 </template>
 <script>
+import NavTopBar from '@/components/nav-top-bar/nav-top-bar.vue';
 import DeviceCard from './components/DeviceCard.vue';
 import PestDiscern from './components/pestDiscern.vue';
 import PestEchart from './components/pestEchart.vue';
@@ -147,6 +151,7 @@ import DeviceData2 from './components/deviceData2.vue';
 
 export default {
   components: {
+    NavTopBar,
     DeviceCard,
     PestDiscern,  
     PestEchart,
@@ -673,7 +678,8 @@ export default {
   display: flex;
   width: 100%;
   height: calc(100vh - 112rpx);
-  padding-top: 112rpx;
+  // 状态栏高度由 nav-top-bar 的占位撑起,这里只留头部与内容的间距
+  padding-top: 24rpx;
   flex-direction: column;
   align-items: center;
   overflow-y: scroll;

+ 2 - 0
pages/sy/devicePhoto.vue

@@ -804,6 +804,8 @@ export default {
 
     .movable-view {
       width: 100%;
+      // 显式高度:movable-view 依赖宽高计算各方向拖动边界,高度缺失会导致放大后无法上下拖动
+      height: 100%;
     }
 
     .image-container {

+ 1 - 1
pages/warning/index.vue

@@ -1,7 +1,7 @@
 <template>
   <view class="warning-center">
     <!-- 顶部导航栏 -->
-    <custom-card>
+    <custom-card background="linear-gradient(180deg, #FFEEEC 0%, #F5F6FA 100%)">
       <block slot="backText">预警中心</block>
     </custom-card>
 

+ 4 - 2
project.config.json

@@ -37,7 +37,9 @@
     "userConfirmedBundleSwitch": false,
     "packNpmManually": false,
     "packNpmRelationList": [],
-    "minifyWXSS": true
+    "minifyWXSS": true,
+    "ignoreDevUnusedFiles": false,
+    "ignoreUploadUnusedFiles": false
   },
   "compileType": "miniprogram",
   "libVersion": "2.16.1",
@@ -50,4 +52,4 @@
     "tabIndent": "insertSpaces",
     "tabSize": 2
   }
-}
+}

+ 2 - 2
util/api.js

@@ -1,8 +1,8 @@
 // let BASE_URL = 'http://114.55.0.7:8002';
 // const BASE_URL='http://8.136.98.49:8002'
-// let BASE_URL = 'https://wx.hnyfwlw.com'
+let BASE_URL = 'https://wx.hnyfwlw.com'
 // let BASE_URL = 'http://192.168.1.107:8000';
-let BASE_URL = 'http://218.28.198.186:10508';
+// let BASE_URL = 'http://218.28.198.186:10508';
 // let BASE_URL = 'https://uat.hnyfwlw.com'
 
 // token 失效全局拦截:清除登录态并跳转登录页(并发请求去重)

+ 1 - 1
util/neutral.js

@@ -1,7 +1,7 @@
 export default {
   // isneutral:true//云飞智控
   isneutral: false, //智控
-  developAPI:'http://218.28.198.186:10508',//测试环境访问地址
+  developAPI:'https://uat.hnyfwlw.com',//测试环境访问地址
   // productAPI:'http://8.136.98.49:8002',//生产环境
   productAPI:'https://api.hnyfwlw.com',//生产环境
   // imageURL:'http://www.hnyfwlw.com:8006',//图片访问路径