Bläddra i källkod

feat: 图片更新

yf_elsa.cui 6 månader sedan
förälder
incheckning
1e12786418

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

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1109 - 0
pages/cb/cbd/equip-set/new-analyse.vue


+ 468 - 0
pages/cb/cbd/equip-set/pestLine.vue

@@ -0,0 +1,468 @@
+<template>
+	<view>
+		<view class="title">虫情趋势矩阵图</view>
+		<view class="shuju_one">
+
+			<view class="canvastishi" v-if="!canvastishiTF && !dataloadingtf">
+				暂无数据
+			</view>
+			<view class="canvastishi" v-if="dataloadingtf">
+				<p class="dataloading">加载中</p>
+			</view>
+			<canvas v-show="canvastishiTF" canvas-id="canvasColumnB" id="canvasColumnB" class="charts"
+				@touchstart="touchLineA($event)" @touchmove="moveLineA($event)" @touchend="touchEndLineA($event)"
+				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>
+		<view class="title">害虫排名</view>
+		<view class="shuju_one">
+
+			<view class="canvastishi" v-if="!canvastishiTF && !dataloadingtf">
+				暂无数据
+			</view>
+			<view class="canvastishi" v-if="dataloadingtf">
+				<p class="dataloading">加载中</p>
+			</view>
+			<canvas v-show="canvastishiTF" canvas-id="canvasColumnC" id="canvasColumnC" class="charts"
+				:style="{'width':cWidth*pixelRatio+'px','height':pestHeight*pixelRatio+'px', 'transform': 'scale('+(1/pixelRatio)+')','margin-left':-cWidth*(pixelRatio-1)/2+'px','margin-top':-cHeight*(pixelRatio-1)/2+'px'}"></canvas>
+		</view>
+
+	</view>
+
+</template>
+
+<script>
+	import uCharts from '../../../../components/js_sdk/u-charts/u-charts/u-charts.js';
+	let canvaColumnA = null;
+	let canvaColumnB = null;
+	const myColor = ['#FF5951', '#66EDED', '#E67B3E', '#6DE28B', '#FFC97A', '#E7EB4B', '#1561F3', '#FA73F5', '#159AFF',
+		'#FA73F5'
+	]
+	const temColor = ['#A2845E', '#018B3F', '#00C7BE']
+
+	export default {
+		data() {
+			return {
+
+				cWidth: '400',
+				cHeight: '400',
+				pixelRatio: 1,
+				canvastishiTF: false, //暂无数据提示
+				dataloadingtf: true, //加载中提示
+				pestHeight: '400',
+			}
+		},
+		props: {
+			start_time: {
+				type: String,
+				default: ''
+			},
+			end_time: {
+				type: String,
+				default: ''
+			},
+			device_type_id: {
+				type: String,
+				default: '35'
+			},
+			d_id: {
+				type: String,
+				default: 0
+			},
+			notify: {
+				type: Number,
+				default: 0
+			}
+		},
+		watch: {
+			notify: {
+				handler() {
+					this.pestLine()
+				}
+			}
+		},
+		computed: {
+
+		},
+		methods: {
+
+			async pestLine() { //害虫趋势折线图
+				this.dataloadingtf = true
+				const res = await this.$myRequest({
+					url: '/api/api_gateway?method=new_gateway.photo_info.pest_order_and_char',
+					data: {
+						device_type_id: this.device_type_id,
+						id: this.d_id,
+						// device_type_id: 35,
+						// id: '11630161',
+						start: +new Date(this.start_time + ' 00:00:00') / 1000,
+						end: +new Date(this.end_time + ' 23:59:59') / 1000,
+						hour_st: '00',
+						hour_ed: '00'
+					}
+				})
+				this.dataloadingtf = false
+				const {
+					char_data,
+					pest_total,
+					at_ah_info: atah
+				} = res
+				
+				if (Object.keys(char_data).length == 0) {
+					this.canvastishiTF = false
+				} else {
+					this.canvastishiTF = true
+					
+					let arr1 = []
+					let arr2 = []
+					let arr3 = []
+					let xtitle = []
+					for (let i = 0; i < atah.length; i++) {
+						xtitle.push(this.formatTime(atah[i].addtime * 1000, 'yyyy-MM-dd'))
+						arr1.push(atah[i].height ? atah[i].height : 0)
+						arr2.push(atah[i].low ? atah[i].low : 0)
+						arr3.push(atah[i].ah ? atah[i].ah : 0)
+
+					}
+					// console.log(arr1)
+					let objList = [{
+						name: '高温',
+						data: arr1
+
+					}, {
+						name: '低温',
+						data: arr2
+					}, {
+						name: '湿度',
+						data: arr3
+					}]
+					Object.keys(char_data).forEach((item, index) => {
+						let countData = []
+						char_data[item].forEach((item1, index1) => {
+							countData.push(item1.sum)
+						})
+						let obj = {
+							name: item,
+
+							data: countData
+						}
+						objList.push(obj)
+					})
+
+					const pestName = Object.keys(pest_total)
+					const dataList = Object.values(pest_total).reverse().map((value, index) => {
+						let i = index % myColor.length
+						
+						return {
+							value,
+							color: myColor[i]
+						}
+					})
+					const pestTotal = [{
+						name: '排名',
+						data: dataList
+					}]
+
+					this.showColumn("canvasColumnB", xtitle, objList)
+					this.showColumnBar("canvasColumnC", pestName.reverse(), pestTotal)
+				}
+			},
+
+
+			showColumn(id, xtitle, xinfo, yAxis) {
+				
+				let _self = this
+				if (xinfo.length > 12) {
+					this.cHeight = xinfo.length * 15
+				}
+				// const canvas = document.getElementById(id);
+				// const ctx = canvas.getContext("2d");
+				const ctx = uni.createCanvasContext(id, this);
+				canvaColumnA = new uCharts({
+					context: ctx,
+					type: 'line',
+					legend: {
+						position: "top"
+					},
+					fontSize: 11,
+					background: '#FFFFFF',
+					pixelRatio: 1,
+					animation: true,
+					dataLabel: false,
+					categories: xtitle,
+					series: xinfo,
+					color: [...temColor, ...myColor],
+					enableScroll: true, //开启图表拖拽功能
+					xAxis: {
+						disableGrid: true,
+						type: 'grid',
+						gridType: 'dash',
+						itemCount: 4, //x轴单屏显示数据的数量,默认为5个
+						scrollShow: true, //新增是否显示滚动条,默认false
+						// scrollAlign: 'left', //滚动条初始位置
+						scrollBackgroundColor: '#F7F7FF', //默认为 #EFEBEF
+						scrollColor: '#DEE7F7', //默认为 #A6A6A6
+					},
+					yAxis: yAxis || {},
+					width: _self.cWidth * 1,
+					height: _self.cHeight * 1,
+					extra: {
+						line: {
+							type: 'curve'
+						}
+					}
+				});
+			},
+			showColumnBar(id, xtitle, xinfo) {
+				console.log('bar:',xtitle, xinfo)
+				let _self = this
+				if (xtitle.length > 12) {
+					this.pestHeight = xtitle.length * 20
+				}
+
+				const ctx = uni.createCanvasContext(id, this);
+				canvaColumnB = new uCharts({
+					context: ctx,
+					type: 'bar',
+
+					legend: {
+						show: false
+					},
+					padding: [5, 25, 5, 0],
+					fontSize: 11,
+					background: '#FFFFFF',
+					pixelRatio: 1,
+					animation: true,
+					// dataLabel: false,
+					categories: xtitle,
+					series: xinfo,
+					// enableScroll: true, //开启图表拖拽功能
+					xAxis: {
+						boundaryGap: "justify",
+						disableGrid: false,
+						axisLine: false
+					},
+					yAxis: {
+
+					},
+					width: _self.cWidth * 1,
+					height: _self.pestHeight * 1,
+					extra: {
+						bar: {
+							type: "group",
+							width: 30,
+
+							barBorderCircle: true,
+							seriesGap: 4,
+							categoryGap: 4
+						}
+					}
+				});
+			},
+			touchLineA(e) {
+				console.log(e)
+				canvaColumnA.scrollStart(e);
+			},
+			moveLineA(e) {
+				canvaColumnA.scroll(e);
+			},
+			touchEndLineA(e) {
+				canvaColumnA.scrollEnd(e);
+				//下面是toolTip事件,如果滚动后不需要显示,可不填写
+				canvaColumnA.showToolTip(e, {
+					format: function(item, category) {
+						return category + ' ' + item.name + ':' + item.data
+					}
+				});
+			},
+			touchLineB(e) {
+				console.log(e)
+				canvaColumnB.scrollStart(e);
+			},
+			moveLineB(e) {
+				canvaColumnB.scroll(e);
+			},
+			touchEndLineB(e) {
+				canvaColumnB.scrollEnd(e);
+
+			},
+		},
+		mounted() {
+			this.cWidth = uni.upx2px(650);
+			this.cHeight = uni.upx2px(500);
+			this.pestLine()
+			console.log('mounted')
+		},
+		onLoad() {
+			console.log('load')
+
+		},
+		onShow() {
+			console.log('show')
+		},
+
+	}
+</script>
+
+<style lang="scss" scoped>
+	page {
+		background: #f7f7f7;
+	}
+
+	.title {
+		color: #999999;
+		margin: 32rpx 0;
+	}
+
+	.date {
+		height: 92rpx;
+		line-height: 92rpx;
+		border-radius: 92rpx;
+		background: #fff;
+		display: flex;
+		justify-content: space-between;
+		margin-top: 16rpx;
+		padding: 0 48rpx;
+		position: relative;
+
+		/deep/.u-calendar__action {
+			display: flex;
+			justify-content: space-around;
+
+			.u-calendar__action__text {
+				line-height: 25px;
+			}
+		}
+	}
+
+	.shuju_one,
+	.shuju_two {
+		// position: absolute;
+		// top: 54px;
+		width: 100%;
+		// left: 5%;
+		// box-shadow: 0 0 10rpx #bcb9ca;
+		padding-top: 20rpx;
+		height: 550rpx;
+		background-color: #fff;
+		border-radius: 24rpx;
+		overflow-y: scroll;
+		position: relative;
+		.canvastishi {
+			font-size: 32rpx;
+			position: absolute;
+			top: 50%;
+			left: 50%;
+			margin-left: -64rpx;
+			margin-top: -21rpx;
+
+			.dataloading:after {
+				overflow: hidden;
+				display: inline-block;
+				vertical-align: bottom;
+				animation: ellipsis 2s infinite;
+				content: "\2026";
+			}
+
+			@keyframes ellipsis {
+				from {
+					width: 2px;
+				}
+
+				to {
+					width: 15px;
+				}
+			}
+		}
+
+
+	}
+
+	.refresh {
+		// position: absolute;
+		// top: 700rpx;
+		// left: 5%;
+		font-size: 12px;
+		float: right;
+		padding: 0 10rpx;
+		height: 40rpx;
+		border-radius: 8rpx;
+		background-color: #018B3F;
+		color: #FFFFFF;
+		line-height: 40rpx;
+		text-align: center;
+	}
+
+	.condition {
+		// position: absolute;
+		// top: 770rpx;
+		display: flex;
+		flex-wrap: wrap;
+		width: 100%;
+		// left: 5%;
+		// box-shadow: 0 0 10rpx #bcb9ca;
+		margin-bottom: 30rpx;
+
+		.scroll-X {
+			width: 100%;
+			margin: 20rpx auto;
+
+			.table {
+				width: 1672px;
+			}
+
+			.tr {
+				display: flex;
+				overflow: hidden;
+
+				.th,
+				.td {
+					display: inline-block;
+					padding: 4rpx;
+					width: 286rpx;
+					text-align: center;
+					height: 52rpx;
+					line-height: 52rpx;
+					border: 2rpx solid #F1F1F1;
+				}
+
+				.th:first-child,
+				.td:first-child {
+					width: 300rpx;
+				}
+			}
+
+			.tr:nth-child(2n-1) {
+				background-color: #f5fff8;
+			}
+
+			.tr:first-child {
+				background-color: #57c878;
+				color: #fff;
+			}
+		}
+
+		.pagenumber {
+			display: flex;
+			margin: 20rpx auto;
+
+			button {
+				width: 150rpx;
+				height: 50rpx;
+				line-height: 50rpx;
+				font-size: 26rpx;
+				text-align: center;
+				background-color: #17BB89;
+				color: #FFFFFF;
+			}
+
+			.pagenumber_page {
+				width: 100rpx;
+				height: 50rpx;
+				line-height: 50rpx;
+				font-size: 26rpx;
+				text-align: center;
+			}
+		}
+	}
+</style>

BIN
static/images/tabBar/device.png


BIN
static/images/tabBar/devicesel.png


BIN
static/images/tabBar/fenbu.png


BIN
static/images/tabBar/fenbusel.png


BIN
static/images/tabBar/gerenzhongxin.png


BIN
static/images/tabBar/gerenzhongxinsel.png


BIN
static/images/tabBar/shebeifenbu.png


BIN
static/images/tabBar/shebeifenbusel.png


BIN
static/images/tabBar/shebeiliebiao.png


BIN
static/images/tabBar/shebeiliebiaosel.png


BIN
static/images/tabBar/user.png


BIN
static/images/tabBar/usersel.png