فهرست منبع

fix:ucharts升级

yf_elsa.cui 5 ماه پیش
والد
کامیت
0e7e0bf9f3

+ 167 - 0
components/Draw/comps/Annotation.vue

@@ -0,0 +1,167 @@
+<template>
+  <div class="tipInfo" :style="inputStyle">
+    <div class="header">
+      <span>人工标注</span>
+    </div>
+    <div class="contentBox">
+      <el-input
+        placeholder="输入标注内容"
+        v-model="currentLabel"
+        maxlength="10"
+        size="mini"
+        clearable
+        @keydown.native="handleKeyDown"
+        ref="labelInput"
+      >
+      </el-input>
+      <div class="near" v-if="nearList.length">
+        最近:
+        <el-tag
+          type="primary"
+          size="mini"
+          effect="plain"
+          v-for="pest in nearList"
+          @click="currentLabel = pest"
+          :key="pest"
+          >{{ pest }}</el-tag
+        >
+      </div>
+      <div class="btnBox">
+        <el-button size="mini" @click="deleteRect">
+          取消操作
+        </el-button>
+
+        <el-button type="primary" size="mini" @click="confirmLabel">
+          保存标注
+        </el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    inputStyle: {
+      type: Object,
+      default: () => ({
+        left: `0px`,
+        top: `0px`
+        // transform: 'translate(-50%, -24px)' // 稍微下移避免遮挡
+      })
+    }
+  },
+  data() {
+    return {
+      nearList: [],
+      currentLabel: ''
+    }
+  },
+  computed: {},
+  created() {},
+  mounted() {
+    this.getNearList()
+    this.$nextTick(() => {
+      this.$refs.labelInput.focus()
+    })
+    // this.$refs.labelInput.addEventListener('keydown', this.handleKeyDown)
+  },
+  beforeDestroy() {
+    // this.$refs.labelInput.removeEventListener('keydown', this.handleKeyDown)
+  },
+  watch: {},
+  methods: {
+    handleKeyDown(e) {
+      if (e.code === 'Enter') {
+        this.confirmLabel()
+      }
+    },
+    getNearList() {
+      let nearList = localStorage.getItem('nearPestList')
+      if (nearList) {
+        this.nearList = JSON.parse(nearList)
+      }
+    },
+    deleteRect() {
+      this.$emit('deleteRect')
+    },
+    confirmLabel() {
+      if (this.currentLabel) {
+        this.$emit('confirmLabel', this.currentLabel)
+        if (this.nearList.indexOf(this.currentLabel) !== -1) {
+          this.nearList.splice(this.nearList.indexOf(this.currentLabel), 1) // 删除原来的
+        }
+        this.nearList.unshift(this.currentLabel) // 添加到最前面
+        localStorage.setItem('nearPestList', JSON.stringify(this.nearList))
+      } else {
+        this.$message({
+          message: '请输入标注内容',
+          type: 'warning'
+        })
+      }
+    }
+  },
+  components: {}
+}
+</script>
+
+<style scoped lang="less">
+.tipInfo {
+  background: #fff;
+  position: absolute;
+  z-index: 4;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+  .header {
+    padding: 18px 20px;
+    border-bottom: 1px solid #ebeef5;
+    box-sizing: border-box;
+  }
+}
+.near {
+  font-size: 12px;
+  margin: 10px 0;
+  height: 20px;
+  overflow: hidden;
+  .el-tag {
+    cursor: pointer;
+    margin-right: 10px;
+  }
+}
+.contentBox {
+  width: 250px;
+  padding: 20px;
+}
+.btnBox {
+  margin-top: 10px;
+  text-align: center;
+}
+.annotation-input {
+  position: fixed;
+  z-index: 100;
+  background: white;
+  padding: 8px;
+  border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.annotation-input input {
+  padding: 4px 8px;
+  border: 1px solid #ddd;
+  border-radius: 3px;
+  min-width: 150px;
+}
+
+.annotation-input button {
+  padding: 4px 8px;
+  background: #4caf50;
+  color: white;
+  border: none;
+  border-radius: 3px;
+  cursor: pointer;
+}
+</style>

+ 143 - 0
components/Draw/comps/panel.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="my-panel">
+    <el-button v-show="reIdentify" class="resetPest" @click="resetPest" size="small" type="primary"
+      >重新识别</el-button
+    >
+    <el-tabs type="border-card" class="infoCard">
+      <el-tab-pane label="图像识别">
+        <div class="info title">
+          <span>{{ title }}</span>
+          <span>数量</span>
+        </div>
+        <div class="info" v-for="(value, key) in pestObj" :key="key">
+          <span>{{ key }}</span>
+          <span>{{ value }}</span>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Panel',
+  props: {
+    rectangles: {
+      type: Array,
+      default: () => []
+    },
+    reIdentify: {
+      type: Boolean,
+      default: true
+    },
+    title: {
+      type: String,
+      default: '目录'
+    }
+  },
+  data() {
+    return {}
+  },
+  computed: {
+    pestObj() {
+      let obj = {}
+      this.rectangles.forEach(item => {
+        if (obj[item.text]) {
+          obj[item.text]++
+        } else {
+          obj[item.text] = 1
+        }
+      })
+      return obj
+    }
+  },
+  created() {},
+  mounted() {
+    let obj = {}
+    this.rectangles.forEach(item => {
+      if (obj[item.text]) {
+        obj[item.text]++
+      } else {
+        obj[item.text] = 0
+      }
+    })
+    console.log('子组件mounted', obj)
+  },
+  watch: {},
+  methods: {
+    resetPest() {
+      // 通知业务上层处理逻辑
+      this.$emit('resetPest')
+    }
+  },
+  components: {}
+}
+</script>
+
+<style scoped lang="less">
+/deep/ .custom-ffffff .el-tabs--border-card {
+  box-shadow: none;
+}
+.my-panel {
+  position: relative;
+  height: 100%;
+}
+.resetPest {
+  position: absolute;
+  top: 4px;
+  right: 9px;
+  z-index: 1;
+}
+.infoCard {
+  position: relative;
+  height: 100%;
+  overflow: auto;
+  box-sizing: border-box;
+  // box-shadow: none !important;
+  border: 1px solid #e4e7ed;
+  /deep/ .el-tabs__content {
+    height: calc(100% - 70px);
+    overflow: auto;
+    padding: 14px 0 !important;
+    &::-webkit-scrollbar {
+      width: 8px; /* 滚动条宽度 */
+    }
+    /* 滚动条轨道 */
+    &::-webkit-scrollbar-track {
+      background: #f6f6f6; /* 轨道背景 */
+      border-radius: 8px; /* 圆角 */
+    }
+
+    /* 滚动条滑块 */
+    &::-webkit-scrollbar-thumb {
+      background: #b6b6b6; /* 滑块颜色 */
+      border-radius: 8px; /* 圆角 */
+    }
+
+    /* 鼠标悬停时的滑块样式 */
+    &::-webkit-scrollbar-thumb:hover {
+      background: #555; /* 悬停时颜色 */
+    }
+  }
+  .info {
+    display: flex;
+    padding: 10px;
+    align-items: center;
+    font-weight: 600;
+    justify-content: space-between;
+    height: 40px;
+    box-sizing: border-box;
+    color: #333;
+    // margin-top: 16px;
+    font-size: 14px;
+    border-bottom: 1px solid #e4e7ed;
+  }
+  .title {
+    background: #ebedf0;
+    margin-top: 0;
+  }
+}
+.infoCard.el-tabs--border-card {
+  box-shadow: none !important;
+}
+</style>

+ 131 - 0
components/Draw/comps/tools.vue

@@ -0,0 +1,131 @@
+<template>
+  <div class="tools float-left">
+    <button
+      title="上一个"
+      @click="changeItem('pre')"
+      class="el-icon-d-arrow-left"
+    ></button>
+    <button
+      title="下一个"
+      @click="changeItem('next')"
+      class="el-icon-d-arrow-right"
+    ></button>
+    <button
+      title="拖拽"
+      :class="[mode == 'drag' ? 'active' : '']"
+      class="yficonfont icon-zhuashou"
+      @click="mode = 'drag'"
+    ></button>
+    <button
+      title="绘制"
+      :class="[mode == 'draw' ? 'active' : '']"
+      class="el-icon-edit-outline"
+      @click="mode = 'draw'"
+    ></button>
+    <button
+      title="保存标注"
+      @click="saveLabel"
+      class="el-icon-circle-check"
+    ></button>
+    <button title="下载" @click="saveImage" class="el-icon-download"></button>
+    <button
+      title="全屏"
+      @click="fullScreen"
+      class="el-icon-full-screen"
+    ></button>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    fullScreenHtml: ['fullScreenHtml']
+  },
+  data() {
+    return {
+      mode: 'draw'
+    }
+  },
+  computed: {},
+  created() {},
+  mounted() {},
+  watch: {},
+  methods: {
+    // 切换上一个下一个
+    changeItem(type) {
+      this.$emit('changeItem', type)
+    },
+    changeMode(type) {
+      this.$emit('changeMode', type)
+    },
+    fullScreen() {
+      const container = this.fullScreenHtml
+      if (container.requestFullscreen) {
+        container.requestFullscreen()
+      } else if (container.mozRequestFullScreen) {
+        container.mozRequestFullScreen()
+      } else if (container.webkitRequestFullscreen) {
+        container.webkitRequestFullscreen()
+      } else if (container.msRequestFullscreen) {
+        container.msRequestFullscreen()
+      }
+    },
+    saveLabel() {
+      this.$emit('saveLabel')
+    }
+  },
+  components: {}
+}
+</script>
+
+<style scoped lang="less">
+@primary-color: #018b3f;
+.controls {
+  position: absolute;
+  top: 0;
+  left: 0;
+  padding: 3px 15px;
+  width: 70%;
+  box-sizing: border-box;
+  // background: rgba(0, 0, 0, 0.3);
+}
+.info {
+  width: 100%;
+  // color: #ffffff;
+  overflow: hidden;
+  .tools {
+    display: flex;
+    background-color: rgba(0, 0, 0, 0.3);
+    // gap: 10px;
+    .active {
+      background: @primary-color;
+    }
+  }
+  .float-right {
+    line-height: 24px;
+  }
+}
+
+.info span {
+  white-space: nowrap;
+  .active {
+    color: @primary-color;
+    margin-right: 5px;
+  }
+}
+
+.info button {
+  font-size: 16px;
+  padding: 4px 8px;
+  background: none;
+  color: white;
+  border: none;
+  border-radius: 3px;
+  cursor: pointer;
+  white-space: nowrap;
+}
+
+.info button:disabled {
+  cursor: not-allowed;
+}
+</style>

+ 674 - 0
components/Draw/index.vue

@@ -0,0 +1,674 @@
+<template>
+  <div class="canvas-container" ref="canvasContainer">
+    <div
+      class="canvas float-left"
+      ref="yfcontainer"
+      v-loading="loading"
+    >
+      <!-- 画布 -->
+      <canvas
+        :style="{width: canvasSize.width + 'px', height: canvasSize.height + 'px'}"
+		canvas-id="yfCanvas" 
+        id="yfCanvas"
+      ></canvas>
+      <!-- 文本标签 -->
+      <!-- <div class="element-layer">
+        <span
+          class="label-link cursor"
+          v-for="tag in rectangles"
+          @click="deleteSpan($event, tag.id)"
+          :key="tag.id"
+          :style="linkStyle(tag)"
+        >
+          <span class="delete" @click.stop="deleteSpan($event, tag.id)">✖</span>
+        </span>
+      </div> -->
+    </div>
+
+  </div>
+</template>
+
+<script>
+import Panel from './comps/panel.vue'
+import Tools from './comps/tools.vue'
+import Annotation from './comps/Annotation.vue'
+let myRatio = 1
+export default {
+  props: {
+    markItem: {
+      type: Object,
+      default: () => ({
+        addr: 'https://bigdata-image.oss-cn-hangzhou.aliyuncs.com/Basics/cbd/666000000000001/2025/7/11/001.jpg',
+		mark:[],
+		is_mark:0,
+		label: "[{'71': [480, 47, 520, 91, 1.0]}, {'71': [513, 102, 545, 155, 1.0]}, {'71': [690, 238, 740, 272, 1.0]}, {'71': [298, 471, 335, 514, 1.0]}, {'71': [250, 379, 300, 407, 1.0]}, {'71': [106, 40, 143, 89, 1.0]}, {'71': [46, 12, 124, 58, 0.99]}, {'71': [2, 90, 41, 133, 0.98]}, {'71': [767, 189, 799, 237, 0.98]}, {'71': [721, 175, 758, 216, 0.92]}, {'71': [685, 132, 727, 166, 0.35]}]",
+	  })
+    },
+    isInitFullScreen: {
+      type: Boolean,
+      default: false
+    },
+    reIdentify: {
+      type: Boolean,
+      default: true
+    },
+    title: {
+      type: String,
+      default: '目录'
+    },
+
+    typeName: {
+      type: Number,
+      default: 1 // 1: 测报灯  2:病虫害可视监测 3:吸虫塔 4:孢子仪
+    }
+  },
+  watch: {
+    markItem: {
+      handler(newVal) {
+        console.log('markItem变化:', newVal, this.ctx)
+        // 当markItem变化时,重新加载图片和标注
+        if (!newVal.addr && this.ctx) {
+          this.clearCanvas()
+          return
+        }
+        if (this.ctx) {
+          this.loadImage()
+        }
+      },
+      deep: true
+    }
+  },
+  components: {
+    Panel,
+    Tools,
+    Annotation
+  },
+  name: 'Draw',
+  computed: {
+    inputStyle() {
+      return {
+        left: `${this.inputPosition.x}px`,
+        top: `${this.inputPosition.y}px`
+      }
+    },
+    canvasStyle() {
+      return {
+        cursor: this.mode === 'draw' ? 'crosshair' : 'grab'
+      }
+    },
+    fullScreenHtml() {
+      return this.$refs.canvasContainer
+    }
+    
+  },
+  data() {
+    return {
+      imgTimer: false, // 图片时间戳
+      ctx: null,
+      scale: 1, // 画布的缩放
+      imageScale: 1, // 图片的原始缩放
+      markScale: 1, // 绘制的缩放
+      baseScale: 0.31,
+      offsetX: 0,
+      offsetY: 0,
+      mode: 'drag', // 'draw' / 'drag'
+      modeLabels: {
+        draw: 'D绘制',
+        drag: 'S拖拽',
+        select: '空格选择'
+      },
+      isDragging: false,
+      isDrawing: false,
+      lastX: 0,
+      lastY: 0,
+      startX: 0,
+      startY: 0,
+      rectangles: [],
+      currentRect: null,
+      selectedRect: null,
+      backgroundImage: null,
+      imageLoaded: false,
+      originalImageSize: { width: 0, height: 0 },
+      isFullScreen: false,
+      imageRect: { x: 0, y: 0, width: 0, height: 0 }, // 存储图片显示区域
+      showInput: false,
+      currentLabel: '',
+      inputPosition: { x: 0, y: 0 },
+      currentRectId: null,
+
+      lastImageScale: 1,
+      loading: true,
+      isMoving: false,
+      colorMap: {}, // 用于存储文本和颜色的映射关系
+      tempColor: '#FFFF00', // 绘制时黄色半透明
+      finalColor: '#FF0000', // 完成后红色半透明
+      changeLeftActive: false,
+      changeRightActive: false,
+      colorIndex: 0,
+      originDataLength: 0, // 用于记录原始数据长度
+      pestLibrary: {},
+	  canvasSize:{width:310,height:279}
+    }
+  },
+  created() {
+    this.getAllPestList()
+  },
+  mounted() {
+    // console.log('初始化markItem:', this.markItem)
+	
+	uni.getSystemInfo({
+	  success: (res) => {
+	    this.canvasSize.width = res.windowWidth-(24*2+16*2)
+	  }
+	}) 
+	this.$nextTick(()=>{
+		
+	})
+  },
+  beforeDestroy() {
+ 
+  },
+  methods: {
+
+    // 切换上一个下一个
+    changeItem(type) {
+  
+	  this.$emit('changeItem', { type: type, isFullScreen: this.isFullScreen })
+    },
+    changeMode(modeType) {
+      this.mode = modeType
+    },
+
+    resetPest() {
+      // 通知业务上层处理逻辑
+      this.exitFullScreen()
+      this.$emit('resetPest')
+    },
+    // 查看原图
+    checkImagePreview() {
+      this.$refs.imgPreview.click()
+    },
+
+    linkStyle(tag) {
+      let x = Math.min(tag.startX, tag.endX)
+      let y = Math.min(tag.startY, tag.endY)
+
+      const top = y * this.scale + this.offsetY - 20 + 'px'
+      const left = x * this.scale + this.offsetX + 'px'
+      return {
+        top,
+        left,
+        color: tag.color,
+        'pointer-events': this.isMoving ? 'none' : 'auto'
+      }
+    },
+    initCanvas() {
+
+		this.ctx = uni.createCanvasContext('yfCanvas', this)
+      console.log(this.ctx)
+      this.loadImage()
+      
+    },
+
+    loadImage() {
+      if (!this.markItem.addr) {
+        this.loading = false
+        return
+      }
+
+	  uni.getImageInfo({
+	    src: this.markItem.addr,
+	    success: (res) => {
+	  		this.imageLoaded = true
+	  		this.backgroundImage = res
+	  		this.originalImageSize = {
+	  		  width: this.backgroundImage.width,
+	  		  height: this.backgroundImage.height
+	  		}
+	  		this.rectangles = [] // 清除之前的标注
+	  		this.selectedRect = null
+	  		this.adjustImagePosition()
+	  		this.initMarkData()
+	  		this.draw()
+	  		this.loading = false
+	    },
+	    fail: (err) => {
+	      this.loading = false
+	      this.$message.error('图片加载失败')
+	    }
+	  })
+
+    },
+
+    adjustImagePosition() {
+      if (!this.imageLoaded) return
+
+      const canvas = this.canvasSize 
+      const canvasAspect = canvas.width / canvas.height
+      const imageAspect = this.originalImageSize.width / this.originalImageSize.height
+
+      // 计算等比缩放的尺寸
+      if (imageAspect > canvasAspect) {
+        this.imageScale = canvas.width / this.originalImageSize.width
+      } else {
+        this.imageScale = canvas.height / this.originalImageSize.height
+      }
+      // this.imageScale = 0.8
+      // 根据图片的宽度来定义缩放比例
+      if (this.originalImageSize.width >= 5000) {
+        this.baseScale = 0.25
+      } else if (this.originalImageSize.width >= 4000) {
+        this.baseScale = 0.31
+      } else if (this.originalImageSize.width < 4000) {
+        this.baseScale = 0.4
+      }
+      // 用来处理图片等比例显示
+      this.markScale = 1 - (this.baseScale - this.imageScale) / this.baseScale
+      // 窗口大小改变的时候使用
+      myRatio = this.imageScale / this.lastImageScale
+
+      this.lastImageScale = this.imageScale
+      // 计算图片显示区域
+      this.imageRect = {
+        x: 0,
+        y: 0,
+        width: this.originalImageSize.width * this.imageScale,
+        height: this.originalImageSize.height * this.imageScale
+      }
+
+      // 初始位置居中
+      this.scale = 1
+      this.offsetX = (canvas.width - this.imageRect.width) / 2
+      this.offsetY = (canvas.height - this.imageRect.height) / 2
+    },
+
+    isPointInImage(x, y) {
+      return x >= 0 && x <= this.imageRect.width && y >= 0 && y <= this.imageRect.height
+    },
+    constrainRect(rect) {
+      // 确保矩形完全在图片区域内
+      const startX = Math.max(0, Math.min(this.imageRect.width, rect.startX))
+      const startY = Math.max(0, Math.min(this.imageRect.height, rect.startY))
+      const endX = Math.max(0, Math.min(this.imageRect.width, rect.endX))
+      const endY = Math.max(0, Math.min(this.imageRect.height, rect.endY))
+
+      return {
+        ...rect,
+        startX,
+        startY,
+        endX,
+        endY
+      }
+    },
+
+
+    handleLabelChange() {
+      this.draw()
+    },
+
+    getRectAt(x, y) {
+      // 从后往前检查,这样最后绘制的矩形会优先被选中
+      for (let i = this.rectangles.length - 1; i >= 0; i--) {
+        const rect = this.rectangles[i]
+        const left = Math.min(rect.startX, rect.endX)
+        const right = Math.max(rect.startX, rect.endX)
+        const top = Math.min(rect.startY, rect.endY)
+        const bottom = Math.max(rect.startY, rect.endY)
+
+        if (x >= left && x <= right && y >= top && y <= bottom) {
+          return rect
+        }
+      }
+      return null
+    },
+
+    deleteSelected() {
+      if (this.selectedRect) {
+        this.deleteRect(this.selectedRect.id)
+        this.selectedRect = null
+      }
+    },
+
+    deleteRect(id) {
+      this.rectangles = this.rectangles.filter(rect => rect.id !== id)
+      if (this.selectedRect && this.selectedRect.id === id) {
+        this.selectedRect = null
+      }
+      this.showInput = false
+      this.currentRectId = null
+      this.draw()
+    },
+    deleteSpan(e, id) {
+      e.preventDefault()
+      e.stopPropagation()
+      this.isDrawing = false
+      this.deleteRect(id)
+    },
+    clearCanvas() {
+      this.loading = false
+      this.imageLoaded = false
+      this.rectangles = []
+      this.selectedRect = null
+      this.showInput = false
+      this.currentRectId = null
+      const canvas = this.canvasSize
+      const ctx = this.ctx
+
+      // 清除画布
+      ctx.clearRect(0, 0, canvas.width, canvas.height)
+    },
+    // 修改后的 draw 方法
+    draw() {
+      const canvas = this.canvasSize
+      const ctx = this.ctx
+
+      // 清除画布
+      ctx.clearRect(0, 0, canvas.width, canvas.height)
+	console.log(this.offsetX, this.offsetY)
+     // 保存当前状态
+     ctx.save()
+     
+     // 应用变换(缩放和平移)
+     ctx.translate(this.offsetX, this.offsetY)
+     ctx.scale(this.scale, this.scale)
+		
+      // 绘制背景图片(不再除以this.scale)
+      if (this.imageLoaded && this.backgroundImage) {
+        const displayWidth = this.originalImageSize.width * this.imageScale
+        const displayHeight = this.originalImageSize.height * this.imageScale
+        // ctx.imageSmoothingEnabled = true
+        // ctx.imageSmoothingQuality = 'high'
+        ctx.drawImage(this.backgroundImage.path, 0, 0, displayWidth, displayHeight)
+		
+	  }
+	console.log('绘制::',this.rectangles)
+      // 绘制所有已保存的矩形(添加约束)
+      this.rectangles.forEach(rect => {
+        const constrainedRect = this.constrainRect(rect)
+        this.drawRectangle(constrainedRect, rect === this.selectedRect)
+      })
+
+      // 绘制当前正在绘制的矩形(添加约束)
+      if (this.currentRect) {
+        const constrainedRect = this.constrainRect(this.currentRect)
+        this.drawRectangle(constrainedRect, false)
+      }
+		
+      // 恢复状态
+      ctx.restore()
+	  ctx.draw()
+    },
+
+    drawRectangle(rect, isSelected) {
+      const ctx = this.ctx
+
+      const x = Math.min(rect.startX, rect.endX)
+      const y = Math.min(rect.startY, rect.endY)
+      const width = Math.abs(rect.endX - rect.startX)
+      const height = Math.abs(rect.endY - rect.startY)
+
+      // 绘制矩形填充
+
+      ctx.strokeStyle = rect.color
+      ctx.lineWidth = 2 / this.scale
+      ctx.strokeRect(x, y, width, height)
+      // 绘制标签文本
+      if (rect.text) {
+        ctx.fillStyle = rect.color
+        ctx.font = `${14 / this.scale}px Arial`
+        ctx.fillText(rect.text, x + 12 / this.scale, y - 6 / this.scale)
+      }
+	
+      
+    },
+
+    // 获取数据
+    getData() {
+      const data = this.rectangles.map(rect => {
+        return {
+          startX: parseFloat((rect.startX / this.markScale).toFixed(2)),
+          startY: parseFloat((rect.startY / this.markScale).toFixed(2)),
+          width: parseFloat((Math.abs(rect.startX - rect.endX) / this.markScale).toFixed(2)),
+          height: parseFloat((Math.abs(rect.startY - rect.endY) / this.markScale).toFixed(2)),
+          text: rect.text
+        }
+      })
+      this.originDataLength = data.length
+      return data
+      // return JSON.stringify(data)
+    },
+    // 处理渲染数据
+    async initMarkData() {
+		
+      const colorList = [
+        '#FF0000',
+        '#00FFC2',
+        '#FF00A8',
+        '#120080',
+        '#BDFF00',
+        '#FFB800',
+        '#5E8000',
+        '#FF5C00',
+        '#00FF75',
+        '#00F0FF',
+        '#0094FF',
+        '#FFFFFF',
+        '#007880',
+        '#00C2FF',
+        '#C74C4C',
+        '#EB00FF'
+      ]
+      if (this.markItem.is_mark === 0) {
+        // 机器标注,取label
+        let aiLabel = []
+        if (this.markItem.label) {
+          aiLabel = JSON.parse(this.markItem.label.replace(/'/g, '"'))
+        }
+        this.rectangles = []
+		console.log('00000',aiLabel)
+        aiLabel.forEach((item, index) => {
+          for (let key in item) {
+            const [startX, startY, endX, endY] = item[key]
+            const text = this.pestLibrary[key]
+            if (!this.colorMap[text]) {
+              this.colorMap[text] = colorList[this.colorIndex % colorList.length]
+              this.colorIndex += 1
+              if (this.colorIndex >= colorList.length) {
+                this.colorIndex = 0
+              }
+            }
+            this.rectangles.push({
+              id: Date.now() + index,
+              text: text,
+              startX: startX * this.imageScale,
+              startY: startY * this.imageScale,
+              endX: endX * this.imageScale,
+              endY: endY * this.imageScale,
+              color: this.colorMap[text],
+              imageScale: this.imageScale
+            })
+          }
+        })
+        this.originDataLength = this.rectangles.length
+      } else {
+        // 人工标注,取mark
+        console.log('this.colorIndex', this.colorIndex)
+        this.rectangles = []
+        this.markItem.mark.map((item, index) => {
+          const { startX, startY, width, height, text } = item
+          if (!this.colorMap[text]) {
+            this.colorMap[text] = colorList[this.colorIndex % colorList.length]
+            this.colorIndex += 1
+            if (this.colorIndex >= colorList.length) {
+              this.colorIndex = 0
+            }
+          }
+          this.rectangles.push({
+            id: Date.now() + index,
+            text: text,
+            startX: Number(startX) * this.markScale,
+            startY: Number(startY) * this.markScale,
+            endX: Number(startX) * this.markScale + Number(width) * this.markScale,
+            endY: Number(startY) * this.markScale + Number(height) * this.markScale,
+            color: this.colorMap[text],
+            imageScale: this.imageScale
+          })
+        })
+        this.originDataLength = this.rectangles.length
+      }
+      console.log('原始rectangles', this.rectangles)
+    },
+    // 保存标注
+    saveLabel() {
+      this.$emit('saveLabel', this.getData())
+    },
+    async getAllPestList() {
+		const res = await this.$myRequest({
+			url: '/api/api_gateway?method=forecast.pest_info.pest_dict',
+			data: {
+				type_name: this.typeName
+			}
+		})
+		// console.log('获取到的病虫害数据', res)
+		this.pestLibrary = res
+		this.initCanvas()
+    },
+ 
+  }
+}
+</script>
+
+<style lang="less" scoped>
+@primary-color: #018b3f !important;
+/deep/ .el-button--primary {
+  background-color: @primary-color !important;
+}
+.canvas-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  background: #ffffff;
+  padding-top: 10px;
+  .virtual-canvas {
+    /* 2025安全隐藏方案 */
+    position: absolute !important;
+    clip: rect(0 0 0 0) !important;
+    pointer-events: none !important;
+    opacity: 0 !important;
+    z-index: -9999 !important;
+  }
+  .canvas {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    background-color: #ffffff;
+    border: 1px solid #e4e7ed;
+    box-sizing: border-box;
+    position: relative;
+  }
+  .canvas-timer {
+    position: absolute;
+    left: 16px;
+    top: 16px;
+    z-index: 2;
+    color: #ffffff;
+    font-size: 23px;
+  }
+  .panel {
+    width: 30%;
+    height: calc(100% - 60px);
+    background: #ffffff;
+    padding-left: 10px;
+    box-sizing: border-box;
+    position: relative;
+    z-index: 3;
+    .save {
+      height: 60px;
+      button {
+        margin: 14px;
+      }
+    }
+  }
+  .controls {
+    // padding: 3px 15px;
+    width: 70%;
+    height: 64px;
+    box-sizing: border-box;
+    position: relative;
+    z-index: 3;
+    // background: rgba(0, 0, 0, 0.3);
+  }
+  .img-timer {
+    position: absolute;
+    left: 0;
+    top: 22px;
+  }
+  .info {
+    // width: 100%;
+    // color: #ffffff;
+    // overflow: hidden;
+    // text-align: center;
+    .tools {
+      // width: 365px;
+      height: 64px;
+      // padding: 0 10%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      // background: rgba(0, 0, 0, 0.6);
+      border-radius: 4px;
+      gap: 10px;
+      // margin: 0 auto;
+      color: rgba(0, 0, 0, 0.6);
+      .active {
+        background: @primary-color;
+        color: #ffffff;
+      }
+      .split {
+        color: #b9b9b9;
+        margin: 0 5px;
+        font-size: 14px;
+      }
+    }
+    .float-right {
+      width: 30%;
+    }
+    .img-preview {
+      display: none;
+      width: 20px;
+      position: absolute;
+    }
+  }
+
+  .info span {
+    white-space: nowrap;
+    .active {
+      color: @primary-color;
+      margin-right: 5px;
+    }
+  }
+
+  .info button {
+    font-size: 26px;
+    padding: 2px 4px;
+    background: none;
+    border: none;
+    border-radius: 3px;
+    cursor: pointer;
+    white-space: nowrap;
+    color: #434343;
+  }
+
+  .info button:disabled {
+    cursor: not-allowed;
+  }
+  .element-layer {
+    .label-link {
+      position: absolute;
+      z-index: 2;
+      user-select: none;
+      pointer-events: auto;
+    }
+  }
+}
+</style>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 5687 - 0
components/js_sdk/u-charts/u-charts/u-charts - 副本.js


+ 215 - 0
components/mark.vue

@@ -0,0 +1,215 @@
+<template>
+  <view class="container" ref="canvas-container">
+	  <span>组件</span>
+    <canvas 
+      canvas-id="myCanvas" 
+      id="myCanvas"
+      @touchstart="touchStart"
+      @touchmove="touchMove"
+      @touchend="touchEnd"
+      :style="{width: canvasWidth + 'px', height: canvasHeight + 'px'}"
+    ></canvas>
+  </view>
+</template>
+
+<script>
+export default {
+
+  data() {
+    return {
+      canvasWidth: 300,
+      canvasHeight: 279,
+      ctx: null,
+      bgImage: null,
+      imageLoaded: false,
+      rectX: 50,
+      rectY: 50,
+      rectWidth: 100,
+      rectHeight: 80,
+      lastDistance: 0, // 双指间距离
+      scale: 1, // 缩放比例
+      lastTouchCenter: { x: 0, y: 0 }, // 上次双指中心点
+      translateX: 0, // X轴平移
+      translateY: 0, // Y轴平移
+      isDragging: false, // 是否正在拖动
+      startDragX: 0, // 拖动开始的X坐标
+      startDragY: 0 // 拖动开始的Y坐标
+    }
+  },
+  mounted() {
+  	uni.getSystemInfo({
+  	  success: (res) => {
+		
+  	    this.canvasWidth = res.windowWidth
+  	    // this.canvasHeight = res.windowHeight * 0.5
+  	    console.log(this.canvasWidth,this.canvasHeight)
+  	    // 创建canvas上下文
+  	    this.ctx = uni.createCanvasContext('myCanvas', this)
+  	    
+  	    // 加载背景图片
+  	    this.loadImage()
+  	  }
+  	})
+  },
+  onReady() {
+	  
+    // 获取系统信息设置canvas尺寸
+    
+  },
+  methods: {
+    loadImage() {
+      // 加载网络图片示例
+      uni.getImageInfo({
+        src: 'https://bigdata-image.oss-cn-hangzhou.aliyuncs.com/Basics/cbd/666000000000001/2025/7/11/001.jpg',
+        success: (res) => {
+			console.log(res)
+          this.bgImage = res
+          this.imageLoaded = true
+          this.drawCanvas()
+        },
+        fail: (err) => {
+          console.error('图片加载失败:', err)
+        }
+      })
+      
+      // 或者使用本地临时文件
+      // uni.downloadFile({
+      //   url: 'https://example.com/background.jpg',
+      //   success: (res) => {
+      //     this.bgImage = res.tempFilePath
+      //     this.imageLoaded = true
+      //     this.drawCanvas()
+      //   }
+      // })
+    },
+    
+    drawCanvas() {
+      if (!this.imageLoaded) return
+      
+      const ctx = this.ctx
+      ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
+      
+      // 绘制背景图片(保持纵横比,适应canvas)
+      // const imgRatio = this.bgImage.width / this.bgImage.height
+      // const canvasRatio = this.canvasWidth / this.canvasHeight
+      // let drawWidth, drawHeight, offsetX = 0, offsetY = 0
+      
+      // if (imgRatio > canvasRatio) {
+      //   drawHeight = this.canvasHeight
+      //   drawWidth = this.bgImage.width * (drawHeight / this.bgImage.height)
+      //   offsetX = (this.canvasWidth - drawWidth) / 2
+      // } else {
+      //   drawWidth = this.canvasWidth
+      //   drawHeight = this.bgImage.height * (drawWidth / this.bgImage.width)
+      //   offsetY = (this.canvasHeight - drawHeight) / 2
+      // }
+      
+      ctx.drawImage(this.bgImage.path, 0, 0, this.bgImage.width, this.bgImage.height)
+      let scaleX = this.canvasWidth /this.bgImage.width
+	  let scaleY = this.canvasHeight /this.bgImage.height
+	  console.log(scaleX,scaleY,'----',this.translateX, this.translateY)
+      // 应用变换(缩放和平移)
+      ctx.save()
+      ctx.translate(this.translateX, this.translateY)
+      ctx.scale(2, 2)
+      
+      // 绘制矩形
+      // ctx.setStrokeStyle('#FF0000')
+      // ctx.setLineWidth(2)
+      // ctx.strokeRect(this.rectX, this.rectY, this.rectWidth, this.rectHeight)
+      
+      ctx.restore()
+      ctx.draw()
+    },
+    
+    // 处理触摸开始事件
+    touchStart(e) {
+      const touches = e.touches
+      
+      if (touches.length === 1) {
+        // 单指拖动
+        this.isDragging = true
+        this.startDragX = this.translateX
+        this.startDragY = this.translateY
+        this.rectX += e.touches[0].x - this.lastTouchCenter.x
+        this.rectY += e.touches[0].y - this.lastTouchCenter.y
+      } else if (touches.length === 2) {
+        // 双指缩放
+        this.isDragging = false
+        const dx = touches[0].x - touches[1].x
+        const dy = touches[0].y - touches[1].y
+        this.lastDistance = Math.sqrt(dx * dx + dy * dy)
+        this.lastTouchCenter = {
+          x: (touches[0].x + touches[1].x) / 2,
+          y: (touches[0].y + touches[1].y) / 2
+        }
+      }
+    },
+    
+    // 处理触摸移动事件
+    touchMove(e) {
+      const touches = e.touches
+      
+      if (this.isDragging && touches.length === 1) {
+        // 单指拖动矩形
+        this.rectX += e.touches[0].x - this.lastTouchCenter.x
+        this.rectY += e.touches[0].y - this.lastTouchCenter.y
+        this.lastTouchCenter = {
+          x: e.touches[0].x,
+          y: e.touches[0].y
+        }
+      } else if (touches.length === 2) {
+        // 双指缩放
+        const dx = touches[0].x - touches[1].x
+        const dy = touches[0].y - touches[1].y
+        const distance = Math.sqrt(dx * dx + dy * dy)
+        
+        // 计算新的缩放比例
+        let newScale = this.scale * (distance / this.lastDistance)
+        // 限制最小和最大缩放比例
+        newScale = Math.max(0.5, Math.min(3, newScale))
+        
+        // 计算缩放中心点相对于当前平移后的坐标
+        const centerX = this.lastTouchCenter.x
+        const centerY = this.lastTouchCenter.y
+        
+        // 计算相对于当前变换的本地坐标
+        // 这部分比较复杂,简化处理
+        this.rectX = centerX - (centerX - this.rectX) * (newScale / this.scale)
+        this.rectY = centerY - (centerY - this.rectY) * (newScale / this.scale)
+        this.rectWidth *= newScale / this.scale
+        this.rectHeight *= newScale / this.scale
+        
+        // 更新缩放和平移
+        this.scale = newScale
+        this.translateX = this.startDragX + (centerX - this.lastTouchCenter.x) * (this.scale - 1)
+        this.translateY = this.startDragY + (centerY - this.lastTouchCenter.y) * (this.scale - 1)
+        
+        this.lastDistance = distance
+        this.lastTouchCenter = {
+          x: centerX,
+          y: centerY
+        }
+      }
+      
+      this.drawCanvas()
+    },
+    
+    // 处理触摸结束事件
+    touchEnd(e) {
+      this.isDragging = false
+    }
+  }
+}
+</script>
+
+<style>
+.container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+</style>

+ 2 - 2
pages/cb/cbd/equip-set/newhistoryfile.vue

@@ -27,7 +27,7 @@
 					disable-scroll=true
 					: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'}"></canvas>
 			</view>
-			<pest-line 
+			<!-- <pest-line 
 			:device_type_id='device_type' 
 			:d_id="d_id"
 			:start_time="start_time"
@@ -35,7 +35,7 @@
 			:notify="notify"
 			>
 				
-			</pest-line>
+			</pest-line> -->
 			<view class="title">监测数据
 
 				<u-button size="mini" :loading='refreshLoading' class="refresh" throttle-time="500" @click="refresh"

+ 4 - 1
pages/cb/cbd/equip-set/photoResults.vue

@@ -13,7 +13,8 @@
 		<view class="images_box">
 
 			<view class="canvas-bg">
-				<img v-if="photos.length>0" :src="photos[active].addr+'?x-oss-process=image/resize,w_130/quality,q_90'" alt=""  @click="examine()"/>
+				<canvas-mark/>
+				<!-- <img v-if="photos.length>0" :src="photos[active].addr+'?x-oss-process=image/resize,w_130/quality,q_90'" alt=""  @click="examine()"/> -->
 			</view>
 			<view class="image-flex" >
 				<view v-for="(item,index) in photos" :key="index">
@@ -42,6 +43,7 @@
 <script>
 	import jsencrypt from '@/components/jsencrypt/jsencrypt.vue';
 	import insect_dict from "../../../../static/data/cbd_pest_library.js"
+	import canvasMark from '@/components/Draw/index.vue';
 	export default {
 		data() {
 			return {
@@ -58,6 +60,7 @@
 				respetLoading:false
 			}
 		},
+		components:{canvasMark},
 		computed:{
 			customStyle(){
 				return {

+ 17 - 1
pages/cb/index/index.vue

@@ -312,7 +312,7 @@
 				item.pur_id = this.equipArr[this.active].pur_id||0
 				let data = JSON.stringify(item)
 			
-				
+				console.log('item',item)
 				if (item.type == 10) {
 					uni.navigateTo({
 						url: '/pages/cb/xy2.0/particulars?info=' + data
@@ -330,6 +330,22 @@
 					uni.navigateTo({
 						url: '/pages/cb/sy/detail?detail=' + data
 					});
+				}else if (item.type == 24) {
+					uni.navigateTo({
+						url: '/pages/cb/zjxydetail/thxydetail?imei=' + item.imei
+					});
+				}else if (item.type == 25) {
+					uni.navigateTo({
+						url: '/pages/cb/xylps/detail?detail=' + data
+					});
+				}else if (item.type == 28) {
+					uni.navigateTo({
+						url: '/pages/cb/smallPest/smallPest?info=' + data
+					});
+				}else if (item.type == 29) {
+					uni.navigateTo({
+						url: `/pages/cb/nlNewXy/nlNewXy?imei=${item.imei}&showId=${item.only_for_show}`
+					});
 				}else if (item.type == 32||item.type == 33||item.type == 34||item.type == 35) {
 					uni.navigateTo({
 						url: '/pages/cb/equip-detail/equip-detail-new?info=' + data

+ 2 - 2
pages/cb/smallPest/smallPest.vue

@@ -241,7 +241,7 @@
 						end: Math.floor(+new Date(this.end_time) / 1000),
 					}
 				})
-				console.log(res)
+				console.log('响应成功:',res)
 				this.dataloadingtf = false
 				this.historydatas = res
 				console.log(this.historydatas)
@@ -372,7 +372,7 @@
 			console.log(JSON.parse(option.info))
 			this.equipInfo = JSON.parse(option.info)
 			this.end_time = +new Date()
-			this.start_time = +new Date() - 30 * 24 * 60 * 60 * 1000
+			this.start_time = +new Date() - 1 * 4 * 60 * 60 * 1000
 			this.cWidth = uni.upx2px(650);
 			this.cHeight = uni.upx2px(500);
 			this.history()

+ 5 - 3
pages/equipList/index.vue

@@ -590,7 +590,7 @@ page {
 .textbox {
   width: 100%;
   height: calc(100vh - 30rpx);
-  padding: 20rpx 30rpx;
+  padding: 20rpx 6rpx;
   box-sizing: border-box;
   border-top-left-radius: 80rpx;
   border-top-right-radius: 80rpx;
@@ -600,6 +600,8 @@ page {
 }
 
 .inputs {
+	width: 95%;
+	margin: 0 auto;
   /deep/.u-content {
     background-color: #fff !important;
   }
@@ -643,8 +645,8 @@ page {
 
   .listbox_left {
     background-color: #fff;
-    padding: 20rpx;
-    width: 180rpx;
+    padding: 20rpx 10rpx 20rpx 0;
+    width: 240rpx;
     overflow: auto;
 
     .listbox_left_item {