ソースを参照

feat: 新增虫害管理页面及相关功能

1. 新增sy模块页面与组件库,包含设备详情、控制、虫害识别分析等页面
2. 新增13个配套静态资源图标与图片
3. 调整接口基础URL配置,优化设备跳转路由
4. 新增设备卡片、悬浮按钮、虫害统计图表等多个复用组件
allen 1 週間 前
コミット
1ab28800d1

+ 26 - 0
pages.json

@@ -112,6 +112,32 @@
 			]
 		},
 		{
+			"root": "pages/sy",
+			"pages": [{
+					"path": "detail", 
+					"style": {
+						"navigationBarTitleText": "设备详情",
+						"enablePullDownRefresh": false,
+						"navigationStyle": "custom"
+					}
+				},{
+					"path": "deviceControl",
+					"style": {
+						"navigationBarTitleText": "设备控制",
+						"enablePullDownRefresh": false,
+						"navigationStyle": "custom"
+					}
+				},{
+					"path": "devicePhoto",
+					"style": {
+						"navigationBarTitleText": "查看图片",
+						"enablePullDownRefresh": false,
+						"navigationStyle": "custom"
+					}
+				}
+			]
+		},
+		{
 			"root": "pages/cb/shuifeizs",
 			"pages": [{
 					"path": "shuifeizs",

+ 1 - 1
pages/equipList2/index.vue

@@ -422,7 +422,7 @@
 						item.addtime = item.uptime;
 						item.type = item.type_id;
 						uni.navigateTo({
-							url: '../cb/equip-detail/equip-detail-new?info=' +
+							url: '../sy/detail?info=' +
 								JSON.stringify(item),
 						});
 						break;

BIN
pages/sy/assets/copy.png


BIN
pages/sy/assets/edit.png


BIN
pages/sy/assets/editBorder.png


BIN
pages/sy/assets/general.png


BIN
pages/sy/assets/offline.png


BIN
pages/sy/assets/online.png


BIN
pages/sy/assets/photoBorder.png


BIN
pages/sy/assets/setting.png


BIN
pages/sy/assets/settingBorder.png


BIN
pages/sy/assets/sim.png


BIN
pages/sy/assets/simBorder.png


+ 133 - 0
pages/sy/components/DeviceCard.vue

@@ -0,0 +1,133 @@
+<template>
+  <view class="device-card__body">
+    <view class="device-card">
+      <view :class=" dataSource.status == 1 ?'online-status':'offline-status'">{{ dataSource.status === 1 ? '在线' : '离线' }}</view>
+      <view class="device-card__title">设备ID</view>
+      <view class="device-card__content">
+        <text class="device-id-text">{{ dataSource.id  }}</text>
+        <img class="copy-icon" :src="copy" @click="copyDeviceId"/>
+      </view>
+    </view>
+    <view class="device-card">
+      <view class="device-card__title">上报时间</view>
+      <view class="device-card__content">{{ dataSource.uptime | timeFormat()}}</view>
+    </view>
+    <view class="device-card">
+      <view class="device-card__title">设备位置</view>
+      <view class="device-card__content address">{{ dataSource.address || '-' }}</view>
+    </view>
+  </view>
+</template>
+<script>
+export default {
+  props: {
+    dataSource: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  data() {
+    return {
+      copy:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/copy.png',
+      title: ''
+    }
+  },
+  methods: {
+    copyDeviceId() {
+      uni.setClipboardData({
+        data: this.dataSource.id,
+        success: () => {
+          uni.showToast({
+            title: '复制成功',
+            icon: 'success'
+          })
+        }
+      })
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+.device-card__body {
+  display: flex;
+  padding: 16px 12px;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 8px;
+  border-radius: 8px;
+  background: linear-gradient(180deg, #EFF 0%, #FFF 23.56%);
+  margin-top: 34rpx;
+  position: relative;
+  .device-card {
+    display: flex;
+    gap: 16px;
+    .device-card__title {
+      width: 120rpx;
+      color: #4E5969;
+      font-size: 14px;
+      font-style: normal;
+      font-weight: 400;
+      text-align: right;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+    }
+    .online-status {
+      width: 136rpx;
+      height: 56rpx;
+      line-height: 56rpx;
+      position: absolute;
+      text-align: center;
+      top: 0;
+      right: 0;
+      background: url('https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/online1.png') no-repeat center center;
+      background-size: 100% 100%;
+      color: #0bbc58;
+      font-family: "Source Han Sans CN VF";
+      font-size: 28rpx;
+    }
+    .offline-status{
+      width: 136rpx;
+      height: 56rpx;
+      line-height: 56rpx;
+      position: absolute;
+      text-align: center;
+      top: 0;
+      right: 0;
+      background: url('https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/offline.png') no-repeat center center;
+      background-size: 100% 100%;
+      color: #ff3546;
+      font-family: "Source Han Sans CN VF";
+      font-size: 28rpx;
+    }
+    .device-card__content {
+      color: #4E5969;
+      font-size: 14px;
+      font-style: normal;
+      font-weight: 400;
+      display: flex;
+      align-items: center;
+      gap: 8rpx;
+      .device-id-text {
+        flex: 1;
+      }
+      .copy-icon {
+        font-family: 'iconfont';
+        font-size: 32rpx;
+        color: #86909C;
+        cursor: pointer;
+        width: 28rpx;
+        height: 28rpx;
+        &:active {
+          opacity: 0.6;
+        }
+      }
+    }
+    .address {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+}
+</style>

+ 48 - 0
pages/sy/components/DeviceControl.vue

@@ -0,0 +1,48 @@
+<template>
+  <view class="device-control-container">
+    <view class="device-status">
+      <view class="status-item">
+        <text class="status-label">环境温度</text>
+        <text class="status-value">{{ deviceStatus.temp }}°C</text>
+      </view>
+      <view class="status-item">
+        <text class="status-label">环境湿度</text>
+        <text class="status-value">{{ deviceStatus.humidity }}%</text>
+      </view>
+      <view class="status-item">
+        <text class="status-label">环境光照</text>
+        <text class="status-value">{{ deviceStatus.light }}Lux</text>
+      </view>
+      <view class="status-item">
+        <text class="status-label">环境湿度</text>
+        <text class="status-value">{{ deviceStatus.moisture }}%</text>
+      </view>
+      <view class="status-item">
+        <text class="status-label">环境湿度</text>
+        <text class="status-value">{{ deviceStatus.pest }}%</text>
+      </view>
+    </view>
+  </view>
+</template>
+<script>
+export default {
+  data() {
+    return {
+      deviceStatus: {
+        temp: 25,
+        humidity: 50,
+        light: 800,
+        moisture: 300,
+        pest: 10
+      }
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+.device-control-container {
+  padding: 24rpx;
+  background-color: #F7F8FA;
+  border-radius: 12rpx;
+}
+</style>

ファイルの差分が大きいため隠しています
+ 1265 - 0
pages/sy/components/deviceData.vue


+ 219 - 0
pages/sy/components/floating-button.vue

@@ -0,0 +1,219 @@
+<template>
+	<view
+		class="floating-container"
+		:style="{ top: position.y + 'px', left: position.x + 'px' }"
+		@touchstart="onTouchStart"
+		@click="onClick"
+	>
+		<view class="floating-icon" :class="{ expanded: isExpanded }">
+			<view class="icon-content">
+				<slot name="icon">
+					<text class="default-icon">+</text>
+				</slot>
+			</view>
+			<view v-if="isExpanded" class="expanded-content">
+				<slot name="expanded">
+				</slot>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'FloatingButton',
+	props: {
+		// 初始位置距离右边的距离
+		right: {
+			type: Number,
+			default: 20
+		},
+		// 初始位置距离底部的距离
+		bottom: {
+			type: Number,
+			default: 100
+		},
+		// 按钮大小
+		size: {
+			type: Number,
+			default: 50
+		},
+		// 是否可拖拽
+		draggable: {
+			type: Boolean,
+			default: true
+		}
+	},
+	data() {
+		return {
+			position: {
+				x: 0,
+				y: 0
+			},
+			isDragging: false,
+			isExpanded: false,
+			startX: 0,
+			startY: 0,
+			lastX: 0,
+			lastY: 0,
+			clickStartTime: 0,
+			hasMoved: false
+		};
+	},
+	mounted() {
+		this.initPosition();
+	},
+	methods: {
+		// 初始化位置
+		initPosition() {
+			const systemInfo = uni.getSystemInfoSync();
+			const screenWidth = systemInfo.windowWidth;
+			const screenHeight = systemInfo.windowHeight;
+
+			this.position = {
+				x: screenWidth - this.right - this.size,
+				y: screenHeight - this.bottom - this.size
+			};
+		},
+
+		// 触摸开始
+		onTouchStart(e) {
+			if (!this.draggable) return;
+
+			const touch = e.touches[0];
+			this.startX = touch.clientX;
+			this.startY = touch.clientY;
+			this.lastX = this.position.x;
+			this.lastY = this.position.y;
+			this.isDragging = true;
+			this.hasMoved = false;
+			this.clickStartTime = Date.now();
+		},
+
+		// 触摸移动
+		onTouchMove(e) {
+			if (!this.draggable || !this.isDragging) return;
+
+			e.preventDefault();
+
+			const touch = e.touches[0];
+			const deltaX = touch.clientX - this.startX;
+			const deltaY = touch.clientY - this.startY;
+
+			// 如果移动超过5像素,认为是拖拽操作
+			if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
+				this.hasMoved = true;
+			}
+
+			const systemInfo = uni.getSystemInfoSync();
+			const screenWidth = systemInfo.windowWidth;
+			const screenHeight = systemInfo.windowHeight;
+
+			// 计算新位置,限制在屏幕范围内
+			let newX = this.lastX + deltaX;
+			let newY = this.lastY + deltaY;
+
+			// 边界限制
+			newX = Math.max(0, Math.min(newX, screenWidth - this.size));
+			newY = Math.max(0, Math.min(newY, screenHeight - this.size));
+
+			this.position = {
+				x: newX,
+				y: newY
+			};
+		},
+
+		// 触摸结束
+		onTouchEnd(e) {
+			if (!this.draggable) return;
+
+			this.isDragging = false;
+
+			// 自动吸附到屏幕边缘
+			this.snapToEdge();
+		},
+
+		// 吸附到屏幕左右边缘
+		snapToEdge() {
+			const systemInfo = uni.getSystemInfoSync();
+			const screenWidth = systemInfo.windowWidth;
+
+			// 判断当前在左半屏还是右半屏
+			const centerX = this.position.x + this.size / 2;
+
+			if (centerX < screenWidth / 2) {
+				// 吸附到左边
+				this.position.x = 10;
+			} else {
+				// 吸附到右边
+				this.position.x = screenWidth - this.size - 10;
+			}
+		},
+
+		// 点击事件
+		onClick(e) {
+			// 如果发生了拖拽移动,不触发点击事件
+			const clickDuration = Date.now() - this.clickStartTime;
+
+			if (this.hasMoved || clickDuration > 300) {
+				return;
+			}
+
+			// 切换展开/收起状态
+			this.isExpanded = !this.isExpanded;
+
+			// 触发父组件事件
+			this.$emit(this.isExpanded ? 'expand' : 'collapse', {
+				expanded: this.isExpanded
+			});
+		},
+
+		// 手动展开
+		expand() {
+			this.isExpanded = true;
+		},
+
+		// 手动收起
+		collapse() {
+			this.isExpanded = false;
+		}
+	}
+};
+</script>
+
+<style scoped>
+.floating-container {
+	position: fixed;
+	z-index: 9999;
+	width: 50px;
+	height: 50px;
+}
+.expanded-content{
+	position: absolute;
+}
+.floating-icon {
+	width: 100%;
+	height: 100%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	transition: all 0.3s ease;
+}
+
+.icon-content {
+	width: 100rpx;
+	height: 100rpx;
+	border-radius: 50%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	transition: all 0.3s ease;
+}
+
+.default-icon {
+	font-size: 24px;
+	color: #ffffff;
+	font-weight: bold;
+}
+
+</style>

+ 214 - 0
pages/sy/components/pestArchive.vue

@@ -0,0 +1,214 @@
+<template>
+  <view class="pest-archive" v-if="pest_info.name">
+    <view class="pest-archive__header">
+      <view class="pest-archive__title">虫害档案</view>
+    </view>
+
+      <view
+        class="pest-card"
+        @tap="handleCardClick()"
+      >
+        <view class="pest-card__image">
+          <image :src="pest_info.img_urls" mode="aspectFill" class="pest-img"></image>
+        </view>
+        <view class="pest-card__info">
+          <view class="pest-card__top">
+            <view class="pest-card__name">
+              名称
+            </view>
+            <view class="pest-card__status">
+              {{ pest_info.name || '未知' }}
+            </view>
+          </view>
+          <view class="pest-card__top">
+            <view class="pest-card__name">
+              害虫信息
+            </view>
+            <view class="pest-card__status" style="height:160rpx;overflow: auto;">
+              {{ pest_info.prevention || '未知' }}
+            </view>
+          </view>
+        </view>
+      </view>
+      <u-popup v-model="showDetail" mode="bottom" border-radius="32">
+        <view class="pest-card__detail">
+          <view class="pest-card__image">
+            <image :src="pest_info.img_urls" mode="aspectFill" class="pest-img"></image>
+          </view>
+          <view>
+            {{ pest_info.name || '未知' }}
+          </view>
+          <view>
+            {{ pest_info.prevention || '未知' }}
+          </view>
+        </view>
+      </u-popup>
+  </view>
+</template>
+
+<script>
+export default {
+  props: {
+    scrollHeight: {
+      type: String,
+      default: '500rpx'
+    },
+    pest_info: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  data() {
+    return {
+      showDetail: false,
+    }
+  },
+  methods: {
+    showPestDetail(){
+      this.showDetail = true;
+    },
+    handleCardClick() {
+      this.showPestDetail()
+      this.$emit('click', this.pest_info)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.pest-archive {
+  width: 100%;
+  padding: 32rpx;
+  margin-top: 24rpx;
+  box-sizing: border-box;
+  background: #fff;
+  border-radius: 16rpx;
+}
+
+.pest-archive__header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24rpx;
+}
+
+.pest-archive__title {
+  font-size: 32rpx;
+  color: #042118;
+  font-family: 'Source Han Sans CN VF';
+  font-weight: 500;
+}
+
+.pest-archive__add {
+  width: 56rpx;
+  height: 56rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #00D26A 0%, #00B55A 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 4rpx 12rpx rgba(0, 210, 106, 0.3);
+
+  .add-icon {
+    font-size: 40rpx;
+    color: #fff;
+    line-height: 1;
+    font-weight: 300;
+  }
+}
+.pest-card__detail{
+  height: 70vh;
+  padding: 40rpx;
+}
+.pest-archive__list {
+  width: 100%;
+}
+.pest-card {
+  display: flex;
+  padding: 24rpx;
+  margin-bottom: 16rpx;
+  background: #F7F8F9;
+  border-radius: 16rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+
+  &:active {
+    opacity: 0.8;
+  }
+}
+
+.pest-card__image {
+  width: 200rpx;
+  height: 200rpx;
+  border-radius: 12rpx;
+  overflow: hidden;
+  flex-shrink: 0;
+  margin-right: 20rpx;
+  background: #e0e0e0;
+
+  .pest-img {
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.pest-card__info {
+  padding: 4rpx 0;
+}
+
+.pest-card__top {
+  display: flex;
+}
+
+.pest-card__name {
+  font-size: 26rpx;
+  color: #999999;
+  font-family: "Source Han Sans CN VF";
+  font-weight: 400;
+  width: 110rpx;
+}
+
+.pest-card__status {
+  width: 270rpx;
+  color: #303133;
+  font-size: 26srpx;
+  font-weight: 500;
+
+  &.status--handled {
+    background: rgba(0, 210, 106, 0.15);
+    color: #00B55A;
+  }
+
+  &.status--handling {
+    background: rgba(255, 149, 0, 0.15);
+    color: #FF9500;
+  }
+
+  &.status--pending {
+    background: rgba(255, 53, 70, 0.15);
+    color: #FF3546;
+  }
+}
+
+.pest-card__time {
+  font-size: 24rpx;
+  color: #86909C;
+  margin-top: 8rpx;
+}
+
+.pest-card__desc {
+  font-size: 26rpx;
+  color: #4E5969;
+  margin-top: 12rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  line-clamp: 2;
+  -webkit-box-orient: vertical;
+  line-height: 1.5;
+}
+</style>

+ 120 - 0
pages/sy/components/pestDiscern.vue

@@ -0,0 +1,120 @@
+<template>
+  <view class="pest-discern" v-if="pests.length">
+    <view class="pest-discern__header">
+      <view class="pest-discern__title">
+        虫害识别
+      </view>
+    </view>
+    <view class="pest-discern__content">
+      <view class="pest-discern__item" v-for="(item,index) in pests" :key="index">
+        <view class="pest-discern__item-title">
+          {{ item.name }}
+        </view>
+        <view class="pest-discern__item-content">
+          <u-line-progress :active-color="item.color" :percent="item.percent" :show-percent="false" height="8"></u-line-progress>
+        </view>
+        <view class="pest-discern__item-count">
+          {{ item.count }}
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+<script>
+export default {
+  props: {
+    total: {
+      type: Number,
+      default: 0
+    },
+    pest_order: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  watch:{
+    pest_order:{
+      handler(val){
+        this.pests = [];
+        let index = 0;
+        for(let key in val){
+          index++;
+          this.pests.push({
+            color: this.colors[index],
+            name: key,
+            count: val[key] || 0,
+            percent: (val[key] / this.total) * 100
+          })
+        }
+      },
+      deep: true
+    }
+  },
+  data() {
+    return {
+      colors:[
+        '#FF5951',
+        '#66EDED',
+        '#E67B3E',
+        '#6DE28B',
+        '#FFC97A',
+        '#E7EB4B',
+        '#1561F3',
+        '#FA73F5',
+        '#159AFF',
+        '#FA73F5'
+      ],
+      pests: []
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.pest-discern {
+  width: 100%;
+  padding: 32rpx;
+  box-sizing: border-box;
+  background: #fff;
+  border-radius: 16rpx;
+}
+.pest-discern__header {
+  padding: 0rpx 0rpx 20rpx 0rpx;
+  font-size: 32rpx;
+  color: #042118;
+  font-family: 'Source Han Sans CN VF';
+  font-weight: 700;
+}
+.pest-discern__title {
+  font-size: 28rpx;
+  color: #042118;
+  font-family: 'Source Han Sans CN VF';
+  font-weight: 500;
+}
+.pest-discern__item {
+  display: flex;
+  width: 100%;
+  margin-bottom: 20rpx;
+  align-items: center;
+  .pest-discern__item-title{
+    width: 120rpx;
+    font-size: 28rpx;
+    color: #042118;
+    font-family: 'Source Han Sans CN VF';
+    font-weight: 400;
+    // 超出隐藏
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+  .pest-discern__item-content{
+    flex: 1;
+    margin: 0 20rpx;
+  }
+  .pest-discern__item-count{
+    width: 80rpx;
+    font-size: 28rpx;
+    color: #042118;
+    text-align: right;
+  }
+}
+</style>

+ 412 - 0
pages/sy/components/pestEchart.vue

@@ -0,0 +1,412 @@
+<template>
+  <view class="pest-echart" v-show="tabs.length">
+    <view class="pest-echart__header">
+      <view class="tab-list">
+        <view
+          v-for="(tab, index) in tabs"
+          :key="index"
+          class="tab-item"
+          :class="{ active: activeTab === index }"
+          @click="switchTab(index)"
+        >
+          {{ tab.name }}
+        </view>
+      </view>
+    </view>
+
+    <view class="pest-echart__content" v-show="dayData.length">
+      <!-- 三个关键时期 -->
+      <view class="period-section">
+        <view class="period-item">
+          <view class="period-label">始见期</view>
+          <view class="period-value">{{ periodData.firstDate }}</view>
+        </view>
+        <view class="period-item">
+          <view class="period-label">高峰期</view>
+          <view class="period-value">{{ periodData.peakDate }}</view>
+        </view>
+        <view class="period-item">
+          <view class="period-label">终见期</view>
+          <view class="period-value">{{ periodData.lastDate }}</view>
+        </view>
+      </view>
+
+      <!-- 图表区域 -->
+      <view class="chart-container">
+        <!-- <canvas
+          canvas-id="pestChart"
+          id="pestChart"
+          class="charts"
+          :style="{'width':cWidth*pixelRatio+'px','height':cHeight*pixelRatio+'px', 'transform': 'scale('+(1/pixelRatio)+')','margin-left':-cWidth*(pixelRatio-1)/2+'px','margin-top':-cHeight*(pixelRatio-1)/2+'px'}"
+          @touchstart="touchChart($event)"
+          @touchmove="moveChart($event)"
+          @touchend="touchEndChart($event)"
+          disable-scroll=true
+        ></canvas> -->
+        <qiun-data-charts type="line" :chartData="chartData" :opts="opts" :canvas2d="true" :inScrollView="true" :ontouch="true" />
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import uCharts from '../../../components/js_sdk/u-charts/u-charts/u-charts.js';
+
+let chartInstance = null;
+
+export default {
+  name: 'PestEchart',
+  props:{
+    pest_order:{
+      type: Object,
+      default: () => ({})
+    },
+    endDate: {
+      type: String,
+      default: ''
+    },
+    d_id: {
+      type: String | Number,
+      default: ''
+    },
+    day:{
+      type: Array,
+      default: () => []
+    },
+    pest:{
+      type: Array,
+      default: () => []
+    },
+  },
+  data() {
+    return {
+      tabs: [],
+      currentPest:'',
+      activeTab: 0,
+      dayData:[],
+      // 三个关键时期数据
+      periodData: {
+        firstDate: '-',
+        peakDate: '-',
+        lastDate: '-'
+      },
+      chartData: {},
+      // canvas 尺寸配置
+      cWidth: 650,
+      cHeight: 400,
+      pixelRatio: 1,
+      opts: {
+        type: 'line',
+        xAxis: {
+          disableGrid: true,
+          itemCount: 3,
+          scrollShow: true
+        },
+        yAxis: {
+          disableGrid: true,
+          gridType: 'dash',
+          splitNumber: 5,
+          min: 0,
+          format: (val) => {
+            return Math.round(val)
+          }
+        },
+        extra: {
+          line: {
+            type: 'curve'
+          },
+          tooltip: {
+            format: {
+              name: '',
+              value: (val) => Math.round(val)
+            }
+          }
+        },
+        legend: {
+        },
+        enableScroll: true
+      },
+    };
+  },
+  watch:{
+    pest_order:{
+      handler(val){
+        this.tabs = [];
+        for(let key in val){
+          this.tabs.push({
+            name: key,
+          })
+        }
+        const name = this.tabs[0]?.name;
+        this.currentPest = name;
+        if(this.currentPest){
+          this.getPestNameDetail(name);
+          this.setChartData();
+        }
+      },
+      deep: true
+    },
+    d_id:{
+      handler(val){
+        val && this.setChartData();
+      },
+      deep: true
+    },
+    endDate:{
+      handler(val){
+        val && this.setChartData();
+      },
+      deep: true
+    },
+    day:{
+      handler(){
+        this.initChart();
+      },
+      deep: true,
+      immediate: true
+    },
+  },
+  mounted() {
+    this.cWidth = uni.upx2px(650);
+    this.cHeight = uni.upx2px(400);
+    this.pixelRatio = uni.getSystemInfoSync().pixelRatio;
+  },
+  methods: {
+    async getPestNameDetail(name){
+      try{
+        const res = await this.$myRequest({
+          url: '/api/pest_name_detail',
+          method: 'POST',
+          data: {
+            name
+          },
+        });
+        this.deviceInfo = res
+        this.$emit('getInfo',res)
+      }catch(err){
+        this.$emit('getInfo',{})
+      }
+    },
+    async setChartData(){
+      if(!this.currentPest){
+        return;
+      }
+      if(!this.d_id || !this.endDate){
+        return;
+      }
+     const res = await this.$myRequest({
+        url: '/api/api_gateway?method=forecast.cbd_analysis.pest_predict_time',
+        method: 'POST',
+        data: {
+          model:'B',
+          d_id: this.d_id,
+          year: this.endDate.split('-')[0],
+          pest: this.currentPest,
+        },
+      });
+      this.periodData = {
+        firstDate: res[0][0],
+        peakDate: res[1][0],
+        lastDate: res[2][0],
+      }
+    },
+    initChart() {
+      this.$nextTick(() => {
+        this.updateChartsData(0);
+      });
+    },
+    drawChart(index) {
+      const dayData = this.day || [];
+      this.dayData = dayData;
+      const pestData = this.pest[index];
+      const ctx = uni.createCanvasContext('pestChart', this);
+      chartInstance = new uCharts({
+        context: ctx,
+        type: 'line',
+        fontSize: 11,
+        legend: {
+          show: false
+        },
+        background: '#FFFFFF',
+        pixelRatio: this.pixelRatio,
+        animation: true,
+        dataLabel: false,
+        categories: dayData,
+        series: [{
+          name: '',
+          data: pestData
+        }],
+        color: ['#0085FF'],
+        xAxis: {
+          disableGrid: false,
+          boundaryGap: 'justify',
+          axisLine: true,
+          lineColor: '#CCCCCC',
+          fontColor: '#999999'
+        },
+        yAxis: {
+          min: 0,
+          minInterval: 1,
+          splitNumber: 4,
+          axisLine: true,
+          lineColor: '#CCCCCC',
+          fontColor: '#999999',
+          gridType: 'dash',
+          gridColor: '#E5E5E5'
+        },
+        width: this.cWidth * this.pixelRatio,
+        height: this.cHeight * this.pixelRatio,
+        extra: {
+          line: {
+            type: 'curve',
+            width: 2,
+            activeType: 'hollow'
+          },
+          tooltip: {
+            showBox: true,
+            bgOpacity: 0.7
+          }
+        }
+      });
+    },
+    touchChart(e) {
+      if (chartInstance) {
+        chartInstance.scrollStart(e);
+      }
+    },
+    moveChart(e) {
+      if (chartInstance) {
+        chartInstance.scroll(e);
+      }
+    },
+    touchEndChart(e) {
+      if (chartInstance) {
+        chartInstance.scrollEnd(e);
+        chartInstance.showToolTip(e, {
+          format: function(item, category) {
+            return category + ' ' + item.name + ':' + item.data
+          }
+        });
+      }
+    },
+    switchTab(index) {
+      this.activeTab = index;
+      const name = this.tabs[index]?.name;
+      this.currentPest = name
+      this.getPestNameDetail(name);
+      this.setChartData();
+      this.$nextTick(() => {
+        this.updateChartsData(index);
+      });
+    },
+    updateChartsData(index){
+      const dayData = this.day || [];
+      this.dayData = dayData;
+      const pestData = this.pest[index];
+      const lineData = {
+        categories: dayData,
+        series: [
+          {
+            name: '',
+            data: pestData
+          }
+        ]
+      };
+      
+      this.chartData = lineData;
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.pest-echart {
+  margin-top: 24rpx;
+  &__header{
+    margin-bottom: 24rpx;
+  }
+}
+
+.tab-list {
+  width: 100%;
+  overflow-x: scroll;
+  white-space: nowrap;
+  overflow-y: hidden;
+  box-sizing: border-box;
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+}
+
+.tab-item {
+  margin-right: 16rpx;
+  font-size: 28rpx;
+  font-family: 'Source Han Sans CN VF', sans-serif;
+  font-weight: 500;
+  color: #999999;
+  display: inline-block;
+  box-sizing: border-box;
+  background: #ffffff;
+  font-family: "Source Han Sans CN VF";
+  font-size: 28rpx;
+  padding: 10rpx 32rpx;
+  border-radius: 8rpx;
+  &:last-child {
+    margin-right: 0;
+  }
+  &.active {
+    color: #0BBC58;
+    font-weight: 700;
+  }
+  &:first-child.active {
+    color: #0BBC58;
+  }
+}
+
+.pest-echart__content {
+  background: #FFFFFF;
+  border-radius: 16rpx;
+  padding: 0 32rpx 32rpx;
+}
+
+.period-section {
+  display: flex;
+  justify-content: space-between;
+  padding: 24rpx;
+  background: #ffffff;
+  border-radius: 12rpx;
+}
+
+.period-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.period-label {
+  font-size: 28rpx;
+  color: #303133;
+  font-family: 'Source Han Sans CN VF', sans-serif;
+  font-weight: 700;
+  margin-bottom: 8rpx;
+}
+
+.period-value {
+  font-size: 24rpx;
+  color: #999999;
+  font-family: 'Source Han Sans CN VF', sans-serif;
+  font-weight: 400;
+}
+
+.chart-container {
+  position: relative;
+  width: 100%;
+  height: 400rpx;
+  border-radius: 12rpx;
+}
+
+.charts {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 223 - 0
pages/sy/components/photoImage.vue

@@ -0,0 +1,223 @@
+<template>
+  <view class="photo-image">
+    <view
+      class="photo-image__tabs"
+      v-if="disableShow"
+    >
+      <view
+        class="photo-image__tab"
+        v-for="(pest, index) in pestList"
+        :key="index"
+        :class="{ active: activeIndexList.includes(index) }"
+        @click="changeTab(index)"
+      >
+        <text class="tab-text">{{ pest.pest_name }}({{ pest.pest_num }})</text>
+      </view>
+    </view>
+    <view class="photo-image__content" v-if="images.length">
+      <view class="photo-image__grid">
+        <view class="photo-image__item" v-for="(item, index) in images" :key="index" @click="handleClick(item)">
+          <view class="photo-image__item-img-container">
+            <image
+              class="photo-image__item-img"
+              :src="item.addr"
+              mode="aspectFill"
+            />
+          </view>
+          <view class="photo-image__item-time">
+            {{ item.addtime }}
+          </view>
+        </view>
+      </view>
+    </view>
+    <view v-else>
+      <u-empty text="暂无图片"></u-empty>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  props:{
+    disableShow:{
+      type: Boolean,
+      default: true
+    },
+    pestList:{
+      type: Array,
+      default: () => []
+    },
+    images:{
+      type: Array,
+      default: () => []
+    },
+    deviceInfo:{
+      type: Object,
+      default: () => ({})
+    },
+    currentYear:{
+      type: String | Number,
+      default: ''
+    },
+    device_type_id:{
+      type: String | Number,
+      default: ''
+    },
+  },
+  data() {
+    return {
+      activeIndexList:[],
+      activePest:[],
+      activeTab: 0,
+      tabs: ['全部', '稻飞虱', '水龟虫', '蝼蛄', '系统','全部', '稻飞虱', '水龟虫', '蝼蛄', '系统'],
+    };
+  },
+  methods: {
+    changeTab(index) {
+      if(this.activeIndexList.includes(index)){
+        this.activePest = this.activePest.filter(item => item !== this.pestList[index])
+        this.activeIndexList = this.activeIndexList.filter(item => item !== index)
+      }else{
+        this.activeIndexList.push(index)
+        this.activePest.push(this.pestList[index])
+      }
+      this.$emit('changeTab', this.activePest)
+    },
+    // 格式化时间
+    formatDate(dateString) {
+      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}`;
+    },
+    handleClick(item) {
+      uni.navigateTo({
+        url: '/pages/sy/devicePhoto?device_id=' + item?.device_id + '&addtime=' + item?.addtime + '&img_id=' + item?.ids + '&id=' + this.deviceInfo.id + '&currentYear=' + this.currentYear+'&device_type_id='+this.device_type_id+'&cmd=sy1'
+      })
+    }
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.photo-image {
+  width: 100%;
+  box-sizing: border-box;
+
+  &__header {
+    margin-bottom: 32rpx;
+    white-space: nowrap;
+
+    &::-webkit-scrollbar {
+      display: none;
+    }
+  }
+
+  &__tabs {
+    box-sizing: border-box;
+    width: 100%;
+    overflow-x: auto;
+    overflow-y: hidden;
+    white-space: nowrap;
+    // 去掉滚动条
+    -ms-overflow-style: none;
+    scrollbar-width: none;
+  }
+
+  .photo-image__content{
+    padding: 30rpx;
+    background: #ffffff;
+    border-radius: 16rpx;
+    margin-top: 30rpx;
+    .photo-image__grid{
+      display: grid;
+      grid-template-columns: repeat(3, 1fr);
+      gap: 20rpx;
+    }
+  }
+  &__tab {
+    position: relative;
+    display: inline-block;
+    box-sizing: border-box;
+    margin-right: 16rpx;
+    background: #ffffff;
+    color: #999999;
+    font-family: "Source Han Sans CN VF";
+    font-size: 28rpx;
+    padding: 10rpx 32rpx;
+    border-radius: 8rpx;
+    &:last-child {
+      margin-right: 0;
+    }
+
+    .tab-text {
+      margin-right: 8rpx;
+    }
+
+    .tab-check {
+      color: #4CAF50;
+      font-weight: bold;
+    }
+
+    &.active {
+      color: #0BBC58;
+    }
+
+    .tab-underline {
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      height: 4rpx;
+      background: #4CAF50;
+      border-radius: 2rpx;
+    }
+  }
+  &__item {
+    position: relative;
+    width: 100%;
+    padding: 0rpx;
+    box-sizing: border-box;
+    display: inline-block;
+    overflow: hidden;
+    border:2rpx solid #e4e7ed;
+    border-radius: 16rpx;
+    &-img-container {
+      width: 100%;
+      padding-bottom: 100%;
+      position: relative;
+      border-radius: 16rpx;
+      overflow: hidden;
+      background-color: #f5f5f5;
+    }
+
+    &-img {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+    }
+
+    &-time {
+      position: absolute;
+      bottom: 0rpx;
+      width: 100%;
+      text-align: center;
+      padding: 0;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      margin-top: 12rpx;
+      font-size: 22rpx;
+      color: #fff;
+      text-align: center;
+      background-color: rgba(0, 0, 0, 0.5);
+    }
+  }
+}
+</style>

+ 738 - 0
pages/sy/detail.vue

@@ -0,0 +1,738 @@
+<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>
+    <view class="operation" @click.stop="isShowOperation = !isShowOperation">操作</view>
+    <view class="operation-container" v-if="isShowOperation" @click="closeOperationHandler">
+      <view class="operation-background"></view>
+      <view class="operation-content">
+        <view class="operation-item" v-if="isShowPhoto" @click="handlePhotoClick">
+          <image :src="photoIcon" class="operation-icon"></image>
+          拍照
+        </view>
+        <view class="operation-item" @click="handleSettingClick">
+          <image :src="settingIcon" class="operation-icon"></image>
+          设置
+        </view>
+        <view class="operation-item" @click="handleSimClick">
+          <image :src="simIcon" class="operation-icon"></image>
+          SIM卡
+        </view>
+        <view class="operation-item" @click="modification">
+          <image :src="editIcon" class="operation-icon"></image>
+          修改
+        </view>
+      </view>
+    </view>
+    <view class="device-detail__body">
+      <DeviceCard
+        :dataSource="deviceInfo"
+        :collectTime="time"
+        :title="deviceInfo.name"
+        :deviceType="deviceType"
+      />
+      <view class="tabs">
+        <view class="tab-container" v-if="isShowTab">
+          <view class="tab-item" :class="activeTab === 'pestAnalysis'?'active':''" @click="handleTabClick('pestAnalysis')" v-if="disableShow">
+            害虫分析
+          </view>
+          <view class="tab-item" :class="activeTab === 'viewImage'?'active':''" @click="handleTabClick('viewImage')">
+            查看图片
+          </view>
+          <view class="tab-item" :class="activeTab === 'deviceData'?'active':''" @click="handleTabClick('deviceData')">
+            设备数据
+          </view>
+        </view>
+        <view class="select-timer-container">
+          <view class="select-year">
+            <view class="select-year-item" @click="showPicker = true">
+              {{ currentYear }}
+              <u-icon name="arrow-down" size="18" class="arrow-down"></u-icon>
+            </view>
+          </view>
+          <view class="tabs-timer-container" @click="showCalendar">
+            <view class="tabs-timer-item">
+              {{startDate}}
+            </view>
+            至
+            <view class="tabs-timer-item">
+              {{endDate}}
+            </view>
+            <u-icon name="calendar" size="36" class="calendar-icon"></u-icon>
+          </view>
+        </view>
+      </view>
+      <view v-if="activeTab === 'pestAnalysis'" class="tab-content">
+        <PestDiscern
+          :total="total"
+          :pest_order="pest_order"
+        />
+        <PestEchart
+          :pest_order="pest_order"
+          @getInfo="getInfo" 
+          :endDate="endDate"
+          :d_id="deviceInfo.d_id"
+          :day="day"
+          :pest="pest"
+        />
+        <PestArchive
+          :pest_info="pestInfo"
+        />
+        <view v-if="!pests.length">
+          <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"
+            :device_type_id="deviceInfo.type_id"
+            :disableShow="disableShow"
+            @changeTab="changeTab"
+            :currentYear="currentYear"
+            :deviceInfo="deviceInfo"
+          />
+        </scroll-view>
+      </view>
+      <view v-if="activeTab === 'deviceData'">
+        <DeviceData
+          :deviceStatic="deviceStatic"
+          :deviceInfo="deviceInfo"
+          :polylineList="polylineList"
+          :deviceHistoryList="deviceHistoryList"
+          :totalPage="totalPage"
+          :currentPage="page"
+          :page_size="page_size"
+          @prevPage="prevPage"
+          @nextPage="nextPage"
+        />
+      </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-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 DeviceData from './components/deviceData.vue';
+
+export default {
+  components: {
+    DeviceCard,
+    PestDiscern,  
+    PestEchart,
+    PestArchive,
+    photoImage,
+    DeviceData,
+  },
+  data(){
+    return {
+      isShowTab:false,
+      showPicker: false,
+      disableShow: false,
+      isShowOperation: false,
+      photoIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/photoIcon.png',
+      editIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/editIcon.png',
+      serviceIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/serviceIcon.png',
+      simIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/simIcon.png',
+      settingIcon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/newImg/home/settingIcon.png',
+      totalPage:0,
+      currentYear: new Date().getFullYear(),
+      selectorRange: [],
+      maxDate: this.formatDate(new Date()),
+      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)),
+      endDate: this.formatDate(new Date()),
+      mode: 'range',
+      imageList: [],
+      pest_order:{},
+      deviceInfo: {},
+      pestInfo: {},
+      time: '',
+      activeTab: 'pestAnalysis',
+      deviceType: '',
+      location: '',
+      total: 0,
+      imgTotal: 0,
+      day: [],
+      pest: [],
+      page:1,
+      is_pest:'',
+      page_size:24,
+      pestList:[],
+      pest_names:'',
+      pests:[],
+      polylineList:[],
+      isShowPhoto:false,
+      deviceHistoryList:[],
+      deviceStatic:{}
+    }
+  },
+  onLoad(options){
+    this.deviceInfo = JSON.parse(options.info);
+    const newVal = this.deviceInfo;
+    if (newVal.device_model == '11'){
+      this.isShowPhoto = true
+    } else if(newVal.device_model == '12'){
+      this.isShowPhoto = false
+    } else if(newVal.device_model == '13'){
+      this.isShowPhoto = true
+    } else if(newVal.device_model == '14'){
+      this.isShowPhoto = true
+    } else if(newVal.device_model == '15'){
+      this.isShowPhoto = false
+    } else{
+      this.isShowPhoto = true
+    }
+    this.getPestAnalysis();
+    this.isShow();
+    const currentYear = new Date().getFullYear();
+    this.selectorRange = [];
+    for(let i = 0;i<50;i++){
+      const item = {
+        label: currentYear - i,
+        id: currentYear - i
+      }
+      this.selectorRange.push(item);
+    }
+  },
+  methods: {
+    handleReachBottom(){
+      if (this.activeTab === 'viewImage') {
+        if(this.imageList.length >= this.imgTotal){
+          return
+        }
+        this.initImageList(true);
+      }
+    },
+    handleSimClick(){
+      // this.showSim = true;
+      //`/pages/deviceDetails/weatherStation/${type}?deviceInfo=${encodeURIComponent(JSON.stringify(this.deviceInfo))}`
+      uni.navigateTo({
+        url:
+          '/pages/deviceDetails/weatherStation/simDetail?deviceInfo=' +
+          encodeURIComponent(JSON.stringify(this.deviceInfo))
+      }); 
+    },
+		modification() {
+      uni.navigateTo({
+        url:
+          '/pages/equipList2/seabox/modification?data=' +
+          JSON.stringify(this.deviceInfo) +
+          '&id=' +
+          this.deviceInfo.type,
+      }); 
+    },
+    async handlePhotoClick(){
+       const res = await this.$myRequest({
+        url: '/api/api_gateway?method=forecast.send_control.admin_device_control',
+        method: 'POST',
+        data: {
+          device_type_id: this.deviceInfo.type,
+          d_id: this.deviceInfo.d_id,
+          cmd: 'takephoto',
+        },
+      });
+      if(res){
+        this.$u.toast('拍照成功')
+      }
+    },
+    handleServiceClick(){
+      uni.navigateTo({
+        url: '/pages/afterSale/addafter?d_id=' + this.deviceInfo.d_id +'&device_id='+this.deviceInfo.id + '&device_type=' + this.deviceInfo.type,
+      })
+    },
+    handleSettingClick(){
+      uni.navigateTo({
+        url: '/pages/sy/deviceControl?deviceId=' + this.deviceInfo.id + '&d_id=' + this.deviceInfo.d_id + '&device_type=' + this.deviceInfo.type,
+      });
+    },
+    closeOperationHandler(){
+      this.isShowOperation = false;
+    },
+    isShow(){
+      // disable == 0 或者 device_model == 15 表示不可以查看
+      if(!this.deviceInfo.d_id){
+        return false;
+      }
+      let showStatus = true;
+      if(this.deviceInfo.device_model == 15 || this.deviceInfo.device_model == 12){
+        showStatus = false;
+      }
+      if(showStatus){
+        this.activeTab = 'pestAnalysis';
+      }else{
+        this.activeTab = 'deviceData';
+      }
+      this.initAction();
+      this.isShowTab = showStatus;
+      if(this.deviceInfo.disable == 0){
+        this.disableShow = false;
+        if(showStatus){
+          this.activeTab = 'viewImage';
+          this.handleTabClick('viewImage');
+        }
+      }else{
+        this.disableShow = true;
+      }
+    },
+    prevPage(e){
+      if(e == 1){
+        return
+      }
+      this.page = e-=1;
+      this.getDeviceHistoryData();
+    },
+    nextPage(e){
+      if(e * this.page_size >= this.totalPage){
+        return
+      }
+      this.page = e+=1;
+      this.getDeviceHistoryData();
+    },
+    changeTab(pestList){
+      let pest_names = pestList.map(item => item.pest_name);
+      this.pest_names = pest_names.join(',');
+      this.initImageList();
+    },
+    confirmHandler(e){
+      this.currentYear = this.selectorRange[e].id;
+      if(this.currentYear == new Date().getFullYear()){
+        // 结束日期为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.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();
+      const month = String(date.getMonth() + 1).padStart(2, '0');
+      const day = String(date.getDate()).padStart(2, '0');
+      return `${year}-${month}-${day}`;
+    },
+    formatMinute(date,type){
+      // 转成毫秒数
+      if(type == 'start'){
+        //转成开始时间的毫秒数
+        date.setHours(0, 0, 0, 0);
+      }else{
+        //转成结束时间的毫秒数
+        date.setHours(23, 59, 59, 999);
+      }
+      const minute = Math.floor(date.getTime() / 1000);
+      return minute;
+    },
+    handleChange(e){
+      this.startDate = e.startDate;
+      this.endDate = e.endDate;
+      this.page = 1;
+      this.initAction();
+    },
+    showCalendar(){
+      this.show = true;
+    },
+    handleBack() {
+      uni.navigateBack({
+        delta: 1
+      });
+    },
+    initAction(){
+      this.pest_order = {}
+      if(this.activeTab === 'pestAnalysis'){
+        this.getPestAnalysis();
+      }else if(this.activeTab === 'viewImage'){
+        this.initPest();
+        this.initImageList();
+      }else if(this.activeTab === 'deviceData'){
+        this.getDeviceData();
+        this.getPolylineData();
+        this.getDeviceHistoryData();
+      }
+    },
+    formatMinute(date,type){
+      // 转成毫秒数
+      if(type == 'start'){
+        //转成开始时间的毫秒数
+        date.setHours(0, 0, 0, 0);
+      }else{
+        //转成结束时间的毫秒数
+        date.setHours(23, 59, 59, 999);
+      }
+      const minute = Math.floor(date.getTime() / 1000);
+      return minute;
+    },
+    handleTabClick(tab) {
+      this.activeTab = tab;
+      this.initAction();
+    },
+    async getDeviceData(){
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=forecast.worm_lamp.device_status_data',
+        method: 'POST',
+        data: {
+          device_id: this.deviceInfo.id,
+        },
+      });
+      this.deviceStatic = res;
+    },
+    async initImageList(isLoadMore){
+      if(isLoadMore){
+        this.page++;
+      }
+      uni.showLoading({
+        title: '加载中',
+      })
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=new_gateway.photo_info.photo_list',
+        method: 'POST',
+        data: {
+          page:this.page,
+          page_size:this.page_size,
+          id: this.deviceInfo.d_id,
+          is_pest: this.is_pest,
+          device_type_id: this.deviceInfo.type_id,
+          pest_names: this.pest_names,
+          start: this.formatMinute(new Date(this.startDate),'start'),// 格式化开始时间YYYY-MM-DD HH:MM:SS
+          end: this.formatMinute(new Date(this.endDate),'end'),// 格式化结束时间YYYY-MM-DD HH:MM:SS
+        },
+      });
+      const data = res?.data || [];
+      uni.hideLoading();
+      this.imgTotal = res?.num || 0;
+      if (isLoadMore) {
+        this.imageList = [...this.imageList, ...data];
+      } else {
+        this.imageList = data;
+      }
+    },
+    async initPest(){
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=forecast.new_cbd.pest_type_list',
+        method: 'POST',
+        data:{
+          page:1,
+          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
+        },
+      });
+      const data = res?.data || [];
+      this.pestList = data
+    },
+    async getPolylineData(){
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=forecast.worm_lamp.device_polyline_data',
+        method: 'POST',
+        data: {
+          device_type_id: this.deviceInfo.type_id,
+          d_id: this.deviceInfo.d_id,
+          start_time: new Date(this.startDate + ' 00:00:00').getTime()/1000,// 转成毫秒
+          end_time: new Date(this.endDate + ' 23:59:59').getTime()/1000,
+        },
+      });
+      const data = res || [];
+      this.polylineList = data
+    },
+    async getDeviceHistoryData(){
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=forecast.worm_lamp.device_history_data',
+        method: 'POST',
+        data: {
+          device_type_id: this.deviceInfo.type_id,
+          device_id: this.deviceInfo.id,
+          start_time: new Date(this.startDate + ' 00:00:00').getTime()/1000,
+          end_time: new Date(this.endDate + ' 23:59:59').getTime()/1000,
+          page: this.page,
+          page_size: this.page_size,
+        },
+      });
+      const data = res?.data || [];
+      this.totalPage = res.counts;
+      this.deviceHistoryList = data
+    },
+    async getPestAnalysis(){
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=forecast.cbd_analysis.analysis_pest_result',
+        method: 'POST',
+        data: {
+        d_id: this.deviceInfo.d_id,
+        start: this.startDate,
+        end: this.endDate,
+        model: 'B'
+        },
+      });
+      const pest_order = res?.pest_order;
+      this.getInfo({});
+      let total = 0;
+      this.pests = [];
+      for(let key in pest_order){
+        total += pest_order[key] || 0;
+        this.pests.push({
+          name: key,
+          percent: (pest_order[key] / total) * 100
+        })
+      }
+      this.pest_order = pest_order;
+      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.total = total;
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+::v-deep .u-calendar__action{
+  display:flex;
+  justify-content: space-between;
+}
+::v-deep .u-hover-class{
+  .u-calendar__content__item__inner{
+    color:#aaa !important;
+  }
+}
+.operation-container{
+  position: fixed;
+  right: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 99;
+}
+.operation{
+  position: fixed;
+  top: 260rpx;
+  right: 0;
+  z-index: 999;
+  width:48rpx;
+  height: 100rpx;
+  line-height: 50rpx;
+  border-radius: 8px 0 0 8px;
+  border-top: 4rpx solid #FFF;
+  border-bottom: 4rpx solid #FFF;
+  border-left: 4rpx solid #FFF;
+  background: #dddfe6a3;
+  color: #515153;
+  text-align: center;
+  font-family: "Source Han Sans CN VF";
+  font-size: 24rpx;
+  font-weight: 500;
+  writing-mode: vertical-rl;
+}
+.operation-background{
+  position: fixed;
+  right: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 998;
+  background: #00000040;
+}
+.operation-content{
+  position: fixed;
+  top: 240rpx;
+  right: 80rpx;
+  height: 126rpx;
+  display: flex;
+  padding: 16rpx 32rpx;
+  align-items: center;
+  gap: 64rpx;
+  z-index: 999;
+  border-radius: 32rpx;
+  border: 2rpx solid #FFF;
+  background: #FFF;
+  backdrop-filter: blur(8rpx);
+  .operation-item{
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    color: #333333;
+    text-align: center;
+    font-family: "Source Han Sans CN VF";
+    font-size: 24rpx;
+    font-weight: 400;
+  }
+  .operation-icon{
+    width: 58rpx;
+    height: 58rpx;
+  }
+}
+.device-detail {
+  display: flex;
+  width: 100%;
+  height: calc(100vh - 112rpx);
+  padding-top: 112rpx;
+  flex-direction: column;
+  align-items: center;
+  overflow-y: scroll;
+  background: linear-gradient(180deg, #ffffff00 0%, #F5F6FA 23.64%, #F5F6FA 100%), linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%);
+  .device-detail__header {
+    width: 100%;
+    font-size: 28rpx;
+    color: #999;
+    color: #042118;
+    font-family: 'Source Han Sans CN VF';
+    font-weight: 700;
+    position: relative;
+    text-align: center;
+    .arrow-left {
+      position: absolute;
+      left: 32rpx;
+      margin-right: 12rpx;
+    }
+  }
+  .device-detail__body {
+    width: calc(100% - 64rpx);
+    margin: 0 auto;
+    border-radius: 16rpx;
+    overflow-x: hidden;
+    overflow-y: auto;
+    // 隐藏滚动条
+    -ms-overflow-style: none;
+    scrollbar-width: none;
+  }
+  .tab-content{
+    width: 100%;
+    padding-bottom: 32rpx;
+  }
+  .tabs {
+    background: #ffffff;
+    margin: 24rpx 0;
+    border-radius: 16rpx;
+    padding: 16rpx 0;
+    padding-top: 0;
+    .tab-container{
+      display: flex;
+      width: 100%;
+      height: 88rpx;
+      line-height: 88rpx;
+      text-align: center;
+      font-size: 28rpx;
+      font-weight: 700;
+      color: #042118;
+      font-family: 'Source Han Sans CN VF';
+    }
+    .select-timer-container{
+      display:flex;
+      align-items: center;
+      padding-left: 32rpx;
+      .select-year{
+        width: 110rpx;
+        text-align: center;
+        height: 64rpx;
+        border-radius: 32rpx;
+        font-family: 'Source Han Sans CN VF';
+        line-height: 64rpx;
+        background: #F1F4F8;
+        padding: 0 32rpx;
+        .select-year-item{
+          color: #656565;
+          font-size: 24rpx;
+          display: flex;
+          align-items: center;
+          justify-content: space-around;
+        }
+      }
+    }
+    .tabs-timer-container{
+      display: flex;
+      align-items: center;
+      width: calc(100% - 256rpx);
+      height: 64rpx;
+      line-height: 64rpx;
+      text-align: center;
+      font-size: 24rpx;
+      font-weight: 500;
+      font-family: 'Source Han Sans CN VF';
+      border-radius: 32rpx;
+      background: #F1F4F8;
+      padding: 0 32rpx;
+      color: #656565;
+      margin: 16rpx;
+      position: relative;
+      .tabs-timer-item{
+        width: 42%;
+        color: #656565;
+        text-align: center;
+        font-family: "Source Han Sans CN VF";
+        font-size: 24rpx;
+        font-weight: 400;
+      }
+      .calendar-icon{
+        margin-left: 24rpx;
+      }
+    }
+    .tab-item {
+      flex: 1;
+      color: #999999;
+      text-align: center;
+      font-family: "Source Han Sans CN VF";
+      font-size: 28rpx;
+      font-weight: 400;
+    }
+    .active{
+      position: relative;
+      color: #303133;
+      text-align: center;
+      font-family: "Source Han Sans CN VF";
+      font-size: 28rpx;
+      font-weight: 700;
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: 0;
+        left: 50%;
+        -webkit-transform: translateX(-50%);
+        transform: translateX(-50%);
+        width: 36rpx;
+        height: 36rpx;
+        border: 6rpx solid #0BBC58;
+        border-radius: 50%;
+        border-color: transparent;
+        border-bottom-color: #0BBC58;
+      }
+    }
+  }
+}
+</style>

+ 553 - 0
pages/sy/deviceControl.vue

@@ -0,0 +1,553 @@
+<template>
+  <view class="device-detail">
+    <view class="device-detail__header">
+      <u-icon
+        size="36"
+        class="arrow-left"
+        name="arrow-left"
+        @click="handleBack"
+      ></u-icon>
+      {{ title }}
+    </view>
+		<u-select v-model="show" :list="list" @confirm="confirmHandler"></u-select>
+    <view class="device-detail__body">
+      <view class="tabs">
+        <view class="tab-container">
+          <view class="tab-item" :class="activeTab === 'pestAnalysis'?'active':''" @click="handleTabClick('pestAnalysis')">
+            管理员
+          </view>
+          <view class="tab-item" :class="activeTab === 'viewImage'?'active':''" @click="handleTabClick('viewImage')">
+            设置
+          </view>
+        </view>
+      </view>
+      <view class="device-detail-content" v-if="activeTab === 'pestAnalysis'">
+        <view class="device-detail-item">
+          <text class="device-detail-label">联网模块</text>
+          <view class="device-detail-btn-container">
+            <view class="device-detail-btn" @click="setDeviceContorl('dtu_reboot')">数据单元重启</view>
+            <view class="device-detail-btn" @click="setDeviceContorl('dtu_update')">数据单元升级</view>
+          </view>
+        </view>
+        <view class="device-detail-item">
+          <text class="device-detail-label">设备操作</text>
+          <view class="device-detail-btn-container">
+            <view class="device-detail-btn" @click="setDeviceContorl('led_on')">开启补光灯</view>
+            <view class="device-detail-btn" @click="setDeviceContorl('led_off')">关闭补光灯</view>
+            <view class="device-detail-btn" @click="setDeviceContorl('takephoto')">测试拍照</view>
+            <view class="device-detail-btn" @click="setDeviceContorl('FB_Test')">卷带测试</view>
+          </view>
+        </view>
+        <view class="device-detail-item">
+          <text class="device-detail-label">强制操作</text>
+          <view class="device-detail-btn-container">
+            <view class="device-detail-btn force-btn" @click="setDeviceContorl('reboot')">控制单元重启</view>
+            <view class="device-detail-btn force-btn" @click="setDeviceContorl('update')">控制单元升级</view>
+          </view>
+        </view>
+      </view>
+      <view class="device-detail-content" style="padding-bottom: 50rpx" v-if="activeTab === 'viewImage'">
+        <view class="device-detail-viewImage">
+          <text class="device-detail-label">设备操作:</text>
+          <text @click="show = true">{{ formart(cmd1) }}</text>
+        </view>
+        <view class="device-detail-viewImage">
+          <text class="device-detail-label">拍照时间:</text>
+          <text @click="show1 = true">{{ fromTime(equipContrlForm.START_TIME) }} - {{ fromTime(equipContrlForm.END_TIME) }}</text>
+        </view>
+        <view class="device-detail-viewImage">
+          <text class="device-detail-label">拍照频率(min):</text>
+          <u-input
+            v-model="equipContrlForm.PHO_T"
+            type="number"
+            :border="false"
+            height="40"
+            auto-height="true"
+            input-align="right"
+          />
+        </view>
+        <view class="device-detail-viewImage">
+          <text class="device-detail-label">数据上传频率(min):</text>
+          <u-input
+            v-model="equipContrlForm.DATT"
+            type="number"
+            :border="false"
+            height="40"
+            auto-height="true"
+            input-align="right"
+          />
+        </view>
+        <view class="device-detail-viewImage">
+          <text class="device-detail-label">是否温控:</text>
+          <u-switch v-model="equipContrlForm.TEMP_LIMIT" size="40" active-color="#0BBC58" active-value="1" inactive-value="0"></u-switch>
+        </view>
+        <view class="device-detail-viewImage"
+          v-if="equipContrlForm.TEMP_LIMIT == '1'"
+        >
+          <text class="device-detail-label">低温设置(℃):{{ equipContrlForm.TEMP_DOWN }}</text>
+        </view>
+        <view class="slider-container"
+          v-if="equipContrlForm.TEMP_LIMIT == '1'"
+        >
+          <view class="slider-min-value">0</view>
+          <view
+            class="custom-progress"
+            @touchstart.stop="onSliderTouchStart($event, 'TEMP_DOWN', 0, 20)"
+            @touchmove.stop.prevent="onSliderTouchMove($event, 0, 20)"
+            @touchend.stop="onSliderTouchEnd"
+            @tap.stop="onSliderTap($event, 'TEMP_DOWN', 0, 20)"
+          >
+            <view class="progress-track">
+              <view class="progress-fill" :style="{ width: getProgressWidth(equipContrlForm.TEMP_DOWN, 0, 20) + '%' }"></view>
+              <view class="progress-thumb" :style="{ left: getProgressWidth(equipContrlForm.TEMP_DOWN, 0, 20) + '%' }"></view>
+            </view>
+          </view>
+          <view class="slider-max-value">20</view>
+        </view>
+        <view class="device-detail-viewImage"
+          v-if="equipContrlForm.TEMP_LIMIT == '1'"
+        >
+          <text class="device-detail-label">高温设置(℃):{{ equipContrlForm.TEMP_UP }}</text>
+        </view>
+        <view class="slider-container"
+          v-if="equipContrlForm.TEMP_LIMIT == '1'"
+        >
+          <view class="slider-min-value">50</view>
+          <view
+            class="custom-progress"
+            @touchstart.stop="onSliderTouchStart($event, 'TEMP_UP', 50, 80)"
+            @touchmove.stop.prevent="onSliderTouchMove($event, 50, 80)"
+            @touchend.stop="onSliderTouchEnd"
+            @tap.stop="onSliderTap($event, 'TEMP_UP', 50, 80)"
+          >
+            <view class="progress-track">
+              <view class="progress-fill" :style="{ width: getProgressWidth(equipContrlForm.TEMP_UP, 50, 80) + '%' }"></view>
+              <view class="progress-thumb" :style="{ left: getProgressWidth(equipContrlForm.TEMP_UP, 50, 80) + '%' }"></view>
+            </view>
+          </view>
+          <view class="slider-max-value">80</view>
+        </view>
+      </view>
+    </view>
+    <view class="device-detail-btn-footer" v-if="activeTab === 'viewImage'">
+      <view class="device-detail-btn" @click="saveSettings">保存</view>
+    </view>
+	  <u-select v-model="show1" mode="mutil-column" :list="list2" @confirm="confirmDateHandler"></u-select>
+  </view>
+</template>
+<script>
+
+export default {
+  data(){
+    return {
+      show: false,
+      show1: false,
+      list2:[],
+      list: [
+        { value: '0001', label: '开机' },
+        { value: '0002', label: '关机' },
+        { value: '0003', label: '拍照' },
+        { value: 'JD_Clear', label: '卷带重置' }
+      ],
+      value: 10,
+      cmd1: '0001',
+      imageList: [],
+      equipContrlForm: {},
+      time: '',
+      activeTab: 'pestAnalysis',
+      title: '设置控制',
+      deviceType: '',
+      location: '',
+      d_id: '',
+      checked: false,
+      sliderField: '',
+      sliderMin: 0,
+      sliderMax: 100,
+      sliderRect: null
+    }
+  },
+  onLoad(options){
+    this.d_id = options.d_id
+    const time1 = [];
+    const time2 = [];
+    for(let i = 0;i< 24;i++){
+      let j = i;
+      if(i < 10){
+        j = '0' + i
+      }
+      time1.push({
+        value: i,
+        label: j + ':00'
+      })
+      time2.push({
+        value: i,
+        label: j + ':00'
+      })
+    }
+    this.list2 = [time1, time2];
+    this.deviceType = options.device_type
+    this.getControlDeviceConfigInfo()
+  },
+  methods: {
+    getProgressWidth(value, min, max) {
+      if (max === min) return 0
+      return ((value - min) / (max - min)) * 100
+    },
+    onSliderTouchStart(e, field, min, max) {
+      this.sliderField = field
+      this.sliderMin = min
+      this.sliderMax = max
+      const query = uni.createSelectorQuery().in(this)
+      query.select('.custom-progress').boundingClientRect(rect => {
+        this.sliderRect = rect
+      }).exec()
+      this.updateSliderValue(e.touches[0].clientX, field, min, max)
+    },
+    onSliderTouchMove(e, min, max) {
+      if (!this.sliderField || !this.sliderRect) return
+      this.updateSliderValue(e.touches[0].clientX, this.sliderField, min, max)
+    },
+    onSliderTouchEnd() {
+      this.sliderField = ''
+    },
+    onSliderTap(e, field, min, max) {
+      const query = uni.createSelectorQuery().in(this)
+      query.select('.custom-progress').boundingClientRect(rect => {
+        this.sliderRect = rect
+        this.updateSliderValue(e.detail.x + rect.left, field, min, max)
+      }).exec()
+    },
+    updateSliderValue(clientX, field, min, max) {
+      if (!this.sliderRect) return
+      let ratio = (clientX - this.sliderRect.left) / this.sliderRect.width
+      ratio = Math.max(0, Math.min(1, ratio))
+      const value = Math.round(min + ratio * (max - min))
+      this.$set(this.equipContrlForm, field, value)
+    },
+    fromTime(time){
+      if(time < 10){
+        time = '0' + time + ':00'
+      }else{
+        time = time + ':00'
+      }
+      return time
+    },
+    confirmDateHandler(e){
+      console.log(e,'e')
+      this.show1 = false
+      this.equipContrlForm.START_TIME = e[0].value
+      this.equipContrlForm.END_TIME = e[1].value
+    },
+    confirmHandler(item){
+      this.cmd1 = item[0].value
+      this.operate()
+    },
+    formart(cmd1){
+      let text = ''
+      this.list.forEach(item => {
+        if(item.value === cmd1){
+          text = item.label
+        }
+      })
+      return text
+    },
+    operate() {
+      this.$myRequest({
+        method: 'POST',
+        url: '/api/api_gateway?method=new_gateway.device_info.send_control',
+        data: {
+          id: this.d_id,
+          device_type_id: this.deviceType,
+          COMMAND: this.cmd1
+        }
+      }).then(res => {
+        console.log(res,'resres')
+        this.show = false
+        if (res) {
+          this.$message.success('指令下发成功')
+        } else {
+          this.$message.error(res.data.data.msg)
+        }
+      })
+    },
+    handleBack() {
+      uni.navigateBack({
+        delta: 1
+      });
+    },
+    handleTabClick(tab) {
+      this.activeTab = tab;
+    },
+    async setDeviceContorl(type){
+      const data = {
+        device_type_id: this.deviceType,
+        d_id: this.d_id,
+        cmd: type,
+      }
+      if(type === 'imei'){
+        data.imei = this.equipContrlForm.imei
+      }else{
+        delete data.imei
+      }
+      this.$myRequest({
+        url: '/api/api_gateway?method=new_gateway.device_info.send_control',
+        method: 'POST',
+        data
+      }).then(res => {
+        if (res) {
+          uni.showToast({
+            title: '设备控制修改成功!',
+            icon: 'success',
+          });
+        } else {
+          uni.showToast({
+            title: '设备控制修改失败',
+            icon: 'error',
+          });
+        }
+      });
+    },
+    saveSettings(){
+      let newForm = Object.assign({}, this.equipContrlForm) // 深拷贝
+      this.$myRequest({
+        url: '/api/api_gateway?method=new_gateway.device_info.update_device_config',
+        method: 'POST',
+        data: {
+          device_type_id: this.deviceType,
+          id: this.d_id,
+          ...newForm,
+          TEMP_LIMIT: newForm.TEMP_LIMIT ? '1' : '0'
+        }
+      }).then(res => {
+        if (res) {
+          // 设备控制修改成功
+          uni.showToast({
+            title: '设备控制修改成功!',
+            icon: 'success',
+          });
+        } else {
+          uni.showToast({
+            title: '设备控制修改失败',
+            icon: 'error',
+          });
+        }
+      })
+    },
+    async getControlDeviceConfigInfo(){
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=new_gateway.device_info.get_device_config',
+        method: 'POST',
+        data: {
+          device_type_id: this.deviceType,
+          id: this.d_id,
+        },
+      });
+      console.log(res,'resresresresressssssss')
+      this.equipContrlForm = res
+    },
+  }
+}
+</script>
+<style scoped lang="scss">
+.device-detail {
+  display: flex;
+  width: 100%;
+  height: calc(100vh - 112rpx);
+  padding-top: 112rpx;
+  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%);
+  .device-detail__header {
+    width: 100%;
+    font-size: 28rpx;
+    color: #999;
+    color: #042118;
+    font-family: 'Source Han Sans CN VF';
+    font-weight: 700;
+    position: relative;
+    text-align: center;
+    .arrow-left {
+      position: absolute;
+      left: 32rpx;
+      margin-right: 12rpx;
+    }
+  }
+  .device-detail__body {
+    width: calc(100% - 64rpx);
+    margin: 0 auto;
+    border-radius: 16rpx;
+    overflow-x: hidden;
+    overflow-y: auto;
+    // 隐藏滚动条
+    -ms-overflow-style: none;
+    scrollbar-width: none;
+  }
+  .tabs {
+    margin: 24rpx 0;
+    border-radius: 16rpx;
+    padding: 16rpx 0;
+    padding-top: 0;
+    .tab-container{
+      display: flex;
+      width: 100%;
+      height: 88rpx;
+      line-height: 88rpx;
+      text-align: center;
+      font-size: 28rpx;
+      font-weight: 700;
+      color: #042118;
+      font-family: 'Source Han Sans CN VF';
+    }
+    .tab-item {
+      margin-right: 40rpx;
+      color:#999999;
+    }
+    .active{
+      position: relative;
+      color: #303133;
+      text-align: center;
+      font-family: "Source Han Sans CN VF";
+      font-size: 28rpx;
+      font-weight: 700;
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: 10rpx;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 100%;
+        height: 36rpx;
+        border-bottom: 6rpx solid #303133;
+      }
+    }
+  }
+  .device-detail-content{
+    display: flex;
+    padding: 24rpx 32rpx;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 20rpx;
+    border-radius: 16rpx;
+    background: #FFF;
+    .device-detail-item{
+      .device-detail-label{
+        color: #303133;
+        font-family: "Source Han Sans CN VF";
+        font-size: 28rpx;
+        font-weight: 400;
+      }
+      .device-detail-btn-container{
+        display: flex;
+        gap: 24rpx;
+        margin-top: 12rpx;
+      }
+      .device-detail-btn{
+        display: flex;
+        padding: 10rpx 16rpx;
+        justify-content: center;
+        align-items: center;
+        gap: 16rpx;
+        border-radius: 16rpx;
+        background: #0BBC58;
+        color: #ffffff;
+        font-family: "Source Han Sans CN VF";
+        font-size: 24rpx;
+        font-weight: 400;
+      }
+      .force-btn{
+        background: #FB4E52;
+      }
+    }
+    .device-detail-viewImage{
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding-bottom: 20rpx;
+    }
+  }
+  .slider-container{
+    position: relative;
+    margin: 10rpx 0 20rpx 0;
+    .slider-min-value{
+      position: absolute;
+      left: 0;
+      top: -50rpx;
+    }
+    .slider{
+      width: 600rpx;
+    }
+    .slider-max-value{
+      position: absolute;
+      right: 0;
+      top: -50rpx;
+    }
+  }
+  .custom-progress{
+    width: 600rpx;
+    padding: 0;
+    .progress-track{
+      position: relative;
+      height: 12rpx;
+      background-color: #ebedf0;
+      border-radius: 6rpx;
+    }
+    .progress-fill{
+      position: absolute;
+      left: 0;
+      top: 0;
+      height: 100%;
+      background-color: #0BBC58;
+      border-radius: 6rpx;
+      transition: width 0.2s;
+    }
+    .progress-thumb{
+      position: absolute;
+      top: 50%;
+      width: 28rpx;
+      height: 28rpx;
+      margin-left: -14rpx;
+      margin-top: -14rpx;
+      border-radius: 50%;
+      background-color: #fff;
+      box-shadow: 0 1px 4px rgba(0,0,0,0.3);
+      transition: left 0.2s;
+    }
+  }
+  .device-detail-btn-footer{
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 112rpx;
+    line-height: 112rpx;
+    text-align: center;
+    font-size: 28rpx;
+    font-weight: 700;
+    color: #042118;
+    font-family: 'Source Han Sans CN VF';
+    background: #ffffff;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    .device-detail-btn{
+      display: flex;
+      height: 80rpx;
+      width: 90%;
+      margin: 0 auto;
+      padding: 0rpx 20rpx;
+      justify-content: center;
+      align-items: center;
+      gap: 8rpx;
+      border-radius: 16rpx;
+      background:#0BBC58;
+      color: #ffffff;
+      text-align: center;
+      font-family: "Source Han Sans CN VF";
+      font-size: 14px;
+      font-style: normal;
+      font-weight: 500;
+    }
+  }
+}
+</style>

+ 910 - 0
pages/sy/devicePhoto.vue

@@ -0,0 +1,910 @@
+<template>
+  <view class="device-photo-page">
+    <!-- 顶部导航栏 -->
+    <view class="device-detail__header">
+      <u-icon
+        size="36"
+        class="arrow-left"
+        name="arrow-left"
+        @click="handleBack"
+      ></u-icon>
+      {{ title }}
+    </view>
+
+    <!-- 日期选择器 -->
+     <view class="date-container">
+        <!-- <view class="select-year">
+          <view class="select-year-item" @click="showPicker = true">
+            {{ currentYear }}
+            <u-icon name="arrow-down" size="18" class="arrow-down"></u-icon>
+          </view>
+        </view> -->
+        <view class="date-picker" @click="show = true">
+          <view class="date-input">
+            <text class="date-label">{{ time_begin || '-' }}</text>
+          </view>
+          <!-- <view class="date-separator">-</view>
+          <view class="date-input">
+            <text class="date-label">{{ time_end || '-' }}</text>
+          </view> -->
+          <u-icon name="calendar" class="calendar"></u-icon>
+        </view>
+    </view>
+    <!-- 主图片区域 -->
+    <view class="main-photo" v-if="currentImg.addr">
+      <movable-area class="movable-area" :style="{ height: containerHeight }">
+        <movable-view
+          class="movable-view"
+          direction="all"
+          scale="true"
+          scale-min="1"
+          scale-max="5"
+        >
+          <view class="image-container">
+            <!-- 原图 -->
+            <image
+              :src="currentImg.addr"
+              class="main-photo-image"
+              mode="widthFix"
+              @load="onImageLoad"
+            />
+            <!-- 虫子标记层 -->
+            <view class="bug-markers" v-if="bugMarkers.length > 0">
+              <view
+                v-for="(marker, index) in bugMarkers"
+                :key="index"
+                class="bug-marker"
+                :style="{
+                  left: marker.x + '%',
+                  top: marker.y + '%',
+                  width: marker.width + '%',
+                  height: marker.height + '%',
+                  borderColor: marker.color
+                }"
+              >
+                <view class="bug-label" :style="{ color: marker.color }">
+                  {{ getPestName(marker.id) }}
+                </view>
+              </view>
+            </view>
+          </view>
+        </movable-view>
+      </movable-area>
+      <view class="photo-timestamp">{{ formatDate(currentImg.addtime) }}</view>
+    </view>
+
+    <!-- 缩略图预览 -->
+    <view class="thumbnail-preview">
+      <view  class="thumbnail-scroll">
+        <view 
+          class="thumbnail-item" 
+          v-for="(item, index) in thumbnails" 
+          :key="index"
+          :class="{ active: img_id == item.ids }"
+          @click="selectThumbnail(item)"
+        >
+          <image 
+            :src="item.addr" 
+            class="thumbnail-image"
+          />
+        </view>
+      </view>
+    </view>
+
+    <!-- 识别结果 -->
+    <view class="recognition-result" v-if="pestList.length">
+      <view class="result-title">当前图片识别结果</view>
+      <!-- 一类害虫 -->
+      <view>
+        <view class="pest-category" v-for="(pest,index) in pestList" :key="index">
+          <view class="category-header">
+            <text class="category-title">{{ pest[0] }}</text>
+            <text class="category-count">数量</text>
+          </view>
+          <view class="pest-item" v-for="(p, i) in pest[1]" :key="i">
+            <view class="pest-info">
+              <view class="pest-color" :style="{ backgroundColor: getPestColor(p.pest_name) }"></view>
+              <text class="pest-name">{{ p.pest_name }}</text>
+            </view>
+            <text class="pest-count">{{ p.pest_num }}</text>
+          </view>
+        </view>
+      </view>
+      <!-- <view v-else>
+        <view class="pest-category" v-for="(pest,index) in pestList" :key="index">
+          <view class="pest-item">
+            <view class="pest-info">
+              <view class="pest-color" :style="{ backgroundColor: getPestColor(pest[1].text) }"></view>
+              <text class="pest-name">{{ pest[1].text }}</text>
+            </view>
+            <text class="pest-count">{{ pest[1].value }}</text>
+          </view>
+        </view>
+      </view> -->
+    </view>
+    <u-empty v-else text="暂无数据"></u-empty>
+		<u-calendar 
+      v-model="show"
+      :mode="mode"
+      @change="handleChange"
+      range-color="#999"
+      btn-type="success"
+      active-bg-color="#0BBC58"
+      range-bg-color="rgba(11,188,88,0.13)"
+      :defaultDate="defaultDate"
+    ></u-calendar>
+		<u-picker v-model="showPicker" mode="selector" :range="selectorRange" range-key="id" :default-selector="[0]" @confirm="confirmHandler"></u-picker>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      show: false,
+      mode: 'date',
+      maxDate: this.formatTime(new Date()),
+      minDate: this.formatTime(new Date(new Date().getFullYear(), 0, 1)),
+      defaultDate: this.formatTime(new Date(new Date().getFullYear(), 0, 1)),
+      showPicker: false,
+      selectorRange: [],
+      title: '查看图片',
+      currentThumbnail: 2,
+      pestYype:{},
+      is_mark:0,
+      device_id: '',
+      time_begin: this.formatTime(new Date(new Date().getFullYear(), 0, 1)),
+      time_end: this.formatTime(new Date()),
+      img_id: '',
+      pest_list:[],
+      pestList:[],
+      colors:[
+        '#FF5951',
+        '#66EDED',
+        '#E67B3E',
+        '#6DE28B',
+        '#FFC97A',
+        '#E7EB4B',
+        '#1561F3',
+        '#FA73F5',
+        '#159AFF',
+        '#FA73F5'
+      ],
+      currentYear:'',
+      pest_list_arr:[],
+      thumbnails: [],
+      addtime:'',
+      currentImg:{},
+      bugMarkers: [],
+      imageWidth: 0,
+      imageHeight: 0,
+      imgOld_id:'',
+      containerHeight: '300px',
+      device_type_id:'',
+      cmd:'sy',
+      pestDict: {}, // 虫子字典 {id: name}
+      pestColorMap: {}, // 害虫颜色映射 {pestName: color}
+    };
+  },
+  async onLoad(options) {
+    const {cmd,img_id,currentYear,addtime,device_type_id,device_id} = options
+    this.device_id = device_id
+    this.imgOld_id = img_id
+    this.currentYear = currentYear
+    this.addtime = addtime;
+    this.device_type_id = device_type_id;
+    this.time_begin = this.addtime;
+    console.log(this.time_begin,'thistime_begin')
+    this.defaultDate = this.addtime;
+    this.time_end = this.addtime;
+    this.selectorRange = [];
+    this.cmd = cmd;
+    const nowYear = new Date().getFullYear();
+    for(let i = 0;i<50;i++){
+      const item = {
+        label: nowYear - i,
+        id: nowYear - i
+      }
+      this.selectorRange.push(item);
+    }
+    this.pestList = [];
+    await this.getPestDict();
+    await this.getPestLevelMap();
+    await this.getPestList();
+    // this.getDevicePhotoDetails();
+  },
+  methods: {
+    getPestName(id){
+      if(this.is_mark == 0){
+        return this.pestDict[id] || id;
+      }
+      return id;
+    },
+    async confirmHandler(e){
+      this.currentYear = this.selectorRange[e].id;
+      if(this.currentYear == new Date().getFullYear()){
+        this.time_begin = this.formatTime(new Date(new Date().getFullYear(), 0, 1));
+        this.time_end = this.formatTime(new Date());
+        this.maxDate = this.formatTime(new Date());
+        this.minDate = this.formatTime(new Date(new Date().getFullYear(), 0, 1));
+      }else{
+        this.time_begin = this.formatTime(new Date(this.currentYear, 0, 1));
+        this.time_end = this.formatTime(new Date(this.currentYear, 11, 31));
+        this.maxDate = this.formatTime(new Date(this.currentYear, 11, 31));
+        this.minDate = this.formatTime(new Date(this.currentYear, 0, 1));
+        this.defaultDate = this.minDate;
+      }
+      this.showPicker = false;
+      this.pestList = [];
+      this.thumbnails = [];
+      await this.getPestList();
+    },
+    async handleChange(e){
+      console.log(e,'e')
+      this.time_begin = e.result;
+      this.time_end = e.result;
+      this.pestList = [];
+      await this.getPestList()
+      this.currentImg = {};
+    },
+    getPestColor(pestName) {
+      // 如果该害虫已有颜色,直接返回
+      if (this.pestColorMap[pestName]) {
+        return this.pestColorMap[pestName];
+      }
+      // 为新害虫分配颜色
+      const usedColors = Object.values(this.pestColorMap);
+      // 找一个未使用的颜色
+      let color;
+      for (const c of this.colors) {
+        if (!usedColors.includes(c)) {
+          color = c;
+          break;
+        }
+      }
+      // 如果所有颜色都用完了,使用哈希值确定颜色
+      if (!color) {
+        const hash = pestName.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
+        color = this.colors[hash % this.colors.length];
+      }
+      this.pestColorMap[pestName] = color;
+      return color;
+    },
+     getLevelDisplayName(level) {
+      const levelMap = {
+        '-1': '其他害虫',
+        1: '一类害虫',
+        2: '二类害虫',
+        3: '三类害虫',
+        4: '四类害虫',
+        5: '五类害虫'
+      };
+      return levelMap[String(level)] || `${level}`;
+    },
+    formatMinute(date,type){
+      // 转成毫秒数
+      if(type == 'start'){
+        //转成开始时间的毫秒数
+        date.setHours(0, 0, 0, 0);
+      }else{
+        //转成结束时间的毫秒数
+        date.setHours(23, 59, 59, 999);
+      }
+      const minute = Math.floor(date.getTime() / 1000);
+      return minute;
+    },
+    async getPestList() {
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=new_gateway.photo_info.photo_list',
+        method: 'POST',
+        data: {
+          page:1,
+          page_size:500,
+          id: this.device_id,
+          start: this.formatMinute(new Date(this.time_begin),'start'),
+          end: this.formatMinute(new Date(this.time_end),'end'),
+          device_type_id: this.device_type_id,
+        },
+      });
+      this.thumbnails = res?.data || [];
+      this.currentImg = {};
+      //如果和旧的id相同,就用旧的id不然就用第一个
+      if(this.thumbnails.find(item => item.ids == this.imgOld_id)){
+        this.img_id = this.imgOld_id
+      }else{
+        this.img_id = this.thumbnails[0].ids
+      }
+      this.getDevicePhotoDetails();
+    },
+    formatTime(dateString) {
+      const date = new Date(dateString);
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, '0');
+      const day = String(date.getDate()).padStart(2, '0');
+      return `${year}-${month}-${day}`;
+    },
+    async getPestLevelMap() {
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=forecast.new_cbd.pest_level_map',
+        method: 'POST',
+      });
+      const pestYype = {}
+      for(let key in res){
+        pestYype[this.getLevelDisplayName(key)] = res[key]
+      }
+      this.pestYype = pestYype
+    },
+    // 获取虫子字典
+    async getPestDict() {
+      try {
+        const res = await this.$myRequest({
+          url: '/api/api_gateway?method=forecast.pest_info.pest_dict',
+          method: 'POST',
+          data:{
+            type_name: '1'
+          }
+        });
+        this.pestDict = res || {};
+      } catch (error) {
+        console.error('获取虫子字典失败:', error);
+        this.pestDict = {};
+      }
+    },
+    formatDate(dateString) {
+      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}`;
+    },
+    async getDevicePhotoDetails() {
+      const res = await this.$myRequest({
+        url: '/api/api_gateway?method=forecast.forecast_system.device_photo_details',
+        method: 'POST',
+        data: {
+          identify_model:'B',
+          cmd:this.cmd,
+          img_id: this.img_id,
+        },
+      });
+      this.currentImg = res;
+      // 处理label参数生成bugMarkers
+      this.processBugMarkers();
+      const pest_list = res.pest_list
+      const pestArr = new Map()
+      const pesAlltList = []
+      for(let key in this.pestYype){
+        for(let j in this.pestYype[key]){
+          const pestName = this.pestYype[key][j]
+          pesAlltList.push(pestName)
+          pest_list.forEach(item => {
+            if(item.pest_name == pestName){
+              if(pestArr.has(key)){
+                pestArr.get(key).push(item)
+              }else{
+                pestArr.set(key, [item])
+              }
+            }
+          })
+        }
+      }
+      pest_list.forEach(item => {
+        if(!pesAlltList.includes(item.pest_name)){
+          if(pestArr.has('其他害虫')){
+            pestArr.get('其他害虫').push(item)
+          }else{
+            pestArr.set('其他害虫', [item])
+          }
+        }
+      })
+      const pestList = []
+      for(let key of pestArr){
+        pestList.push(key)
+      }
+      if (this.currentImg.is_mark != 0) {
+        this.processBugMarkers();
+      }
+      this.pestList = pestList
+    },
+    onImageLoad(e) {
+      this.imageWidth = e.detail.width;
+      this.imageHeight = e.detail.height;
+
+      // 计算容器显示高度(根据屏幕宽度和图片宽高比)
+      const systemInfo = uni.getSystemInfoSync();
+      const screenWidth = systemInfo.windowWidth;
+      const containerWidth = screenWidth - 24; // 减去左右margin (24rpx ≈ 12px)
+      const displayHeight = (containerWidth / this.imageWidth) * this.imageHeight;
+
+      this.containerHeight = displayHeight + 'px';
+      console.log('图片尺寸:', this.imageWidth, this.imageHeight, '容器高度:', this.containerHeight);
+
+      // 图片加载完成后处理bugMarkers
+      this.processBugMarkers();
+    },
+    
+    processBugMarkers() {
+      if (this.currentImg.is_mark === 0) {
+        if (!this.currentImg.label) {
+          console.log('没有label参数');
+          this.bugMarkers = [];
+          return;
+        }
+
+        // 如果没有图片尺寸,先设置默认值
+        if (!this.imageWidth || !this.imageHeight) {
+          console.log('没有图片尺寸,使用默认值');
+          this.imageWidth = 1000;
+          this.imageHeight = 1000;
+        }
+
+        try {
+          // 根据原图宽度计算缩放比例
+          let scaleRatio;
+          if (this.imageWidth >= 5000) {
+            scaleRatio = 1;
+          } else if (this.imageWidth >= 4000) {
+            scaleRatio = 1;
+          } else {
+            scaleRatio = 1;
+          }
+          // 处理label参数,将单引号替换为双引号
+          let labelStr = this.currentImg.label;
+          labelStr = labelStr.replace(/'/g, '"');
+          console.log(labelStr,'labelStrlabelStr')
+          const label = JSON.parse(labelStr);
+          const markers = [];
+
+          console.log('原图宽度:', this.imageWidth, '缩放比例:', scaleRatio);
+
+          label.forEach(item => {
+            for (let key in item) {
+              const [x1, y1, x2, y2] = item[key];
+              // 使用缩放比例计算标记位置和尺寸
+              const x = (x1 * scaleRatio / this.imageWidth) * 100;
+              const y = (y1 * scaleRatio / this.imageHeight) * 100;
+              const width = ((x2 - x1) * scaleRatio / this.imageWidth) * 100;
+              const height = ((y2 - y1) * scaleRatio / this.imageHeight) * 100;
+
+              // 从字典中获取虫子名字(尝试字符串和数字两种类型)
+              const pestName = this.pestDict[key] || this.pestDict[String(key)] || key;
+              const color = this.getPestColor(pestName);
+
+              markers.push({
+                id: pestName, // 显示虫子名字
+                x,
+                y,
+                width,
+                height,
+                color
+              });
+            }
+          });
+          this.bugMarkers = markers;
+        } catch (error) {
+          console.error('处理label参数失败:', error);
+          this.bugMarkers = [];
+        }
+      } else {
+        // this.pestList = [];
+        const markers = [];
+        // 根据原图宽度计算缩放比例
+        let scaleRatio = 3.1;
+        // if (this.imageWidth >= 5000) {
+        //   scaleRatio = 4;
+        // } else if (this.imageWidth >= 4000) {
+        //   scaleRatio = 1;
+        // } else {
+        //   scaleRatio = 2.5;
+        // }
+        // const pestArr = new Map()
+        this.currentImg.mark.map((item, index) => {
+          // if(pestArr.has(item.text)){
+          //   pestArr.set(item.text,{
+          //     value: pestArr.get(item.text).value+=1,
+          //     text: item.text
+          //   })
+          // }else{
+          //   pestArr.set(item.text, {
+          //     value: 1,
+          //     text: item.text
+          //   })
+          // }
+          // const pestList = []
+          // for(let key of pestArr){
+          //   pestList.push(key)
+          // }
+          // this.pestList = pestList
+          this.is_mark = 1
+          const { startX, startY,text } = item;
+          // 使用缩放比例计算标记位置和尺寸
+          const x = item.width > 0 ? (startX * scaleRatio / this.imageWidth) * 105 : ((startX + item.width) * scaleRatio / this.imageWidth) * 105;
+          const y = item.height > 0 ? (startY * scaleRatio / this.imageHeight) * 105 : ((startY + item.height) * scaleRatio / this.imageHeight) * 105;
+          const width = (item.width > 0 ? item.width : -item.width) * scaleRatio / this.imageWidth * 100;
+          const height = (item.height > 0 ? item.height : -item.height) * scaleRatio / this.imageHeight * 100;
+
+          // 从字典中获取虫子名字(尝试字符串和数字两种类型)
+          const pestName = text;
+          const color = this.getPestColor(text);
+
+          markers.push({
+            id: pestName, // 显示虫子名字
+            x,
+            y,
+            width,
+            height,
+            color
+          });
+        });
+        this.bugMarkers = markers;
+      }
+    },
+    handleBack() {
+      uni.navigateBack({
+        delta: 1
+      });
+    },
+    goBack() {
+      uni.navigateBack();
+    },
+    selectThumbnail(item) {
+      this.img_id = item.ids;
+      this.getDevicePhotoDetails();
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+::v-deep .u-calendar__action{
+  display:flex;
+  justify-content: space-between;
+}
+::v-deep .u-hover-class{
+  .u-calendar__content__item__inner{
+    color:#aaa !important;
+  }
+}
+.device-photo-page {
+  background-color: #F5F5F5;
+  min-height: 100vh;
+  width: 100%;
+  padding-top: 112rpx;
+  background: linear-gradient(180deg, #ffffff00 0%, #F5F6FA 23.64%, #F5F6FA 100%), linear-gradient(102deg, #BFEADD 6.77%, #B8F1E7 40.15%, #B9EEF5 84.02%);
+  .device-detail__header {
+    width: 100%;
+    font-size: 28rpx;
+    color: #999;
+    color: #042118;
+    font-family: 'Source Han Sans CN VF';
+    font-weight: 700;
+    position: relative;
+    text-align: center;
+    .arrow-left {
+      position: absolute;
+      left: 32rpx;
+      margin-right: 12rpx;
+    }
+  }
+
+  /* 顶部导航栏 */
+  .nav-bar {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 24rpx;
+    background-color: #E6F7EF;
+
+    .nav-left {
+      .back-icon {
+        font-size: 36rpx;
+        color: #042118;
+      }
+    }
+
+    .nav-title {
+      font-size: 32rpx;
+      font-weight: 700;
+      color: #042118;
+    }
+
+    .nav-right {
+      display: flex;
+      align-items: center;
+
+      .more-icon {
+        font-size: 32rpx;
+        color: #042118;
+        margin-right: 24rpx;
+      }
+
+      .eye-icon {
+        font-size: 32rpx;
+      }
+    }
+  }
+  .date-container{
+    padding: 0 20rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .select-year{
+      width: 150rpx;
+      height: 80rpx;
+      display: flex;
+      align-items: center;
+      justify-content: space-around;
+      color: #656565;
+      font-size: 26rpx;
+      line-height: 80rpx;
+      text-align: center;
+      border-radius: 48rpx;
+      background-color: #FFFFFF;
+      margin-right: 10rpx;
+      padding: 0 26rpx;
+      .select-year-item{
+        width: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+      }
+    }
+  }
+  /* 日期选择器 */
+  .date-picker {
+    display: flex;
+    align-items: center;
+    padding: 20rpx 24rpx;
+    background-color: #FFFFFF;
+    margin-bottom: 24rpx;
+    width: 100%;
+    margin:20rpx auto;
+    border-radius: 48rpx;
+    position: relative;
+    .date-input {
+      display: flex;
+      flex-direction: column;
+      width: 100%;
+      text-align: center;
+      .date-label {
+        font-size: 24rpx;
+        color: #999999;
+        margin-bottom: 8rpx;
+      }
+
+      .date-value {
+        font-size: 28rpx;
+        color: #042118;
+        font-weight: 500;
+      }
+    }
+    .calendar {
+      position: absolute;
+      right: 24rpx;
+      top: 50%;
+      transform: translateY(-50%);
+      font-size: 32rpx;
+      color: #999999;
+    }
+    .date-separator {
+      margin: 0 24rpx;
+      font-size: 28rpx;
+      color: #999999;
+    }
+  }
+
+  /* 主图片区域 */
+  .main-photo {
+    position: relative;
+    margin: 0 24rpx 24rpx;
+    background-color: #FFFFFF;
+    border-radius: 16rpx;
+    padding: 0;
+    overflow: hidden;
+    .movable-area {
+      width: 100%;
+      overflow: hidden;
+    }
+
+    .movable-view {
+      width: 100%;
+    }
+
+    .image-container {
+      position: relative;
+      width: 100%;
+    }
+
+    .main-photo-image {
+      width: 100%;
+      display: block;
+    }
+
+    /* 虫子标记层 */
+    .bug-markers {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      pointer-events: none;
+    }
+
+    /* 虫子标记 */
+    .bug-marker {
+      position: absolute;
+      border: 2rpx solid;
+      border-radius: 4rpx;
+    }
+
+    /* 虫子标记标签 */
+    .bug-label {
+      position: absolute;
+      top: -24rpx;
+      left: 0;
+      padding: 2rpx 6rpx;
+      border-radius: 4rpx;
+      font-size: 20rpx;
+      color: white;
+      white-space: nowrap;
+    }
+
+    /* 虫子标记层 */
+    .bug-markers {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      pointer-events: none;
+    }
+
+    /* 虫子标记 */
+    .bug-marker {
+      position: absolute;
+      border: 2rpx solid;
+      border-radius: 4rpx;
+      pointer-events: auto;
+    }
+
+    /* 虫子标记标签 */
+    .bug-label {
+      position: absolute;
+      top: -30rpx;
+      left: 0;
+      padding: 4rpx 8rpx;
+      border-radius: 4rpx;
+      font-size: 20rpx;
+      font-weight: bold;
+    }
+
+    .photo-timestamp {
+      position: absolute;
+      bottom: 0rpx;
+      left: 0rpx;
+      width:100%;
+      background-color: rgba(0, 0, 0, 0.6);
+      color: #FFFFFF;
+      padding: 8rpx 16rpx;
+      border-radius: 8rpx;
+      font-size: 24rpx;
+    }
+  }
+
+  /* 缩略图预览 */
+  .thumbnail-preview {
+    margin: 0 24rpx 24rpx;
+
+    .thumbnail-scroll {
+      gap: 16rpx;
+      padding-bottom: 16rpx;
+      box-sizing: border-box;
+      white-space: nowrap;
+      overflow-x: auto;
+      overflow-y: hidden;
+      width: 100%;
+      // 隐藏滚动条
+      -ms-overflow-style: none;
+      scrollbar-width: none;
+      .thumbnail-item {
+        width: 120rpx;
+        height: 120rpx;
+        border-radius: 8rpx;
+        overflow: hidden;
+        border: 2rpx solid transparent;
+        display: inline-block;
+        box-sizing: border-box;
+        margin-right: 10rpx;
+        &.active {
+          border-color: #0BBC58;
+        }
+
+        .thumbnail-image {
+          width: 100%;
+          height: 100%;
+          object-fit: cover;
+        }
+      }
+    }
+  }
+
+  /* 识别结果 */
+  .recognition-result {
+    margin: 0 24rpx 24rpx;
+    background-color: #FFFFFF;
+    border-radius: 16rpx;
+    padding: 24rpx;
+
+    .result-title {
+      font-size: 28rpx;
+      font-weight: 700;
+      color: #042118;
+      margin-bottom: 24rpx;
+    }
+
+    /* 害虫类别 */
+    .pest-category {
+      margin-bottom: 32rpx;
+      border: 1px solid #E4E7ED;
+      border-radius: 16rpx;
+      &:last-child {
+        margin-bottom: 0;
+      }
+
+      .category-header {
+        display: flex;
+        justify-content: space-between;
+        margin-bottom: 16rpx;
+        background: #F6F8FC;
+        padding: 18rpx 24rpx;
+        .category-title {
+          font-size: 24rpx;
+          font-weight: 500;
+          color: #042118;
+        }
+
+        .category-count {
+          font-size: 24rpx;
+          color: #999999;
+        }
+      }
+
+      .pest-item {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 12rpx;
+        padding: 6rpx 24rpx;
+        &:last-child {
+          margin-bottom: 0;
+        }
+
+        .pest-info {
+          display: flex;
+          align-items: center;
+
+          .pest-color {
+            width: 16rpx;
+            height: 16rpx;
+            border-radius: 50%;
+            margin-right: 12rpx;
+          }
+
+          .pest-name {
+            font-size: 24rpx;
+            color: #042118;
+          }
+        }
+
+        .pest-count {
+          font-size: 24rpx;
+          color: #042118;
+        }
+      }
+    }
+  }
+}
+</style>

+ 2 - 2
util/api.js

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