瀏覽代碼

修改了部分已知bug和新增批次管理

zmj 1 月之前
父節點
當前提交
f8db888f82

+ 4 - 0
package.json

@@ -45,12 +45,15 @@
     "element-china-area-data": "^5.0.2",
     "element-ui": "2.15.14",
     "file-saver": "2.0.5",
+    "flv.js": "^1.6.2",
     "fuse.js": "6.4.3",
     "highlight.js": "9.18.5",
+    "html2canvas": "^1.4.1",
     "js-beautify": "1.13.0",
     "js-cookie": "3.0.1",
     "jsencrypt": "3.0.0-rc.1",
     "nprogress": "0.2.0",
+    "qrcode": "^1.5.4",
     "quill": "2.0.2",
     "screenfull": "5.0.2",
     "seedrandom": "^3.0.5",
@@ -76,6 +79,7 @@
     "connect": "3.6.6",
     "eslint": "7.15.0",
     "eslint-plugin-vue": "7.2.0",
+    "html-webpack-plugin": "^4.5.2",
     "lint-staged": "10.5.3",
     "sass": "1.32.13",
     "sass-loader": "10.1.1",

+ 44 - 0
src/api/base/GrapeDiseaseReport.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询AI农作物病害诊断报告列表
+export function listGrapeDiseaseReport(query) {
+  return request({
+    url: '/base/GrapeDiseaseReport/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询AI农作物病害诊断报告详细
+export function getGrapeDiseaseReport(id) {
+  return request({
+    url: '/base/GrapeDiseaseReport/' + id,
+    method: 'get'
+  })
+}
+
+// 新增AI农作物病害诊断报告
+export function addGrapeDiseaseReport(data) {
+  return request({
+    url: '/base/GrapeDiseaseReport',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改AI农作物病害诊断报告
+export function updateGrapeDiseaseReport(data) {
+  return request({
+    url: '/base/GrapeDiseaseReport',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除AI农作物病害诊断报告
+export function delGrapeDiseaseReport(id) {
+  return request({
+    url: '/base/GrapeDiseaseReport/' + id,
+    method: 'delete'
+  })
+}

+ 44 - 0
src/api/base/batch.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询批次管理列表
+export function listBatch(query) {
+  return request({
+    url: '/base/batch/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询批次管理详细
+export function getBatch(id) {
+  return request({
+    url: '/base/batch/' + id,
+    method: 'get'
+  })
+}
+
+// 新增批次管理
+export function addBatch(data) {
+  return request({
+    url: '/base/batch',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改批次管理
+export function updateBatch(data) {
+  return request({
+    url: '/base/batch',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除批次管理
+export function delBatch(id) {
+  return request({
+    url: '/base/batch/' + id,
+    method: 'delete'
+  })
+}

+ 44 - 0
src/api/base/certificate.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询合格证列表
+export function listCertificate(query) {
+  return request({
+    url: '/base/certificate/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询合格证详细
+export function getCertificate(id) {
+  return request({
+    url: '/base/certificate/' + id,
+    method: 'get'
+  })
+}
+
+// 新增合格证
+export function addCertificate(data) {
+  return request({
+    url: '/base/certificate',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改合格证
+export function updateCertificate(data) {
+  return request({
+    url: '/base/certificate',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除合格证
+export function delCertificate(id) {
+  return request({
+    url: '/base/certificate/' + id,
+    method: 'delete'
+  })
+}

+ 44 - 0
src/api/base/report.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询检测报告列表
+export function listReport(query) {
+  return request({
+    url: '/base/report/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询检测报告详细
+export function getReport(id) {
+  return request({
+    url: '/base/report/' + id,
+    method: 'get'
+  })
+}
+
+// 新增检测报告
+export function addReport(data) {
+  return request({
+    url: '/base/report',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改检测报告
+export function updateReport(data) {
+  return request({
+    url: '/base/report',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除检测报告
+export function delReport(id) {
+  return request({
+    url: '/base/report/' + id,
+    method: 'delete'
+  })
+}

+ 53 - 0
src/api/base/speciesInfo.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 查询物种基础信息列表
+export function listSpeciesInfo(query) {
+  return request({
+    url: '/base/speciesInfo/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询物种基础信息详细
+export function getSpeciesInfo(id) {
+  return request({
+    url: '/base/speciesInfo/' + id,
+    method: 'get'
+  })
+}
+
+// 新增物种基础信息
+export function addSpeciesInfo(data) {
+  return request({
+    url: '/base/speciesInfo',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改物种基础信息
+export function updateSpeciesInfo(data) {
+  return request({
+    url: '/base/speciesInfo',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除物种基础信息
+export function delSpeciesInfo(id) {
+  return request({
+    url: '/base/speciesInfo/' + id,
+    method: 'delete'
+  })
+}
+
+// 统计基础数据
+export function getCount(data) {
+  return request({
+    url: '/base/speciesInfo/count',
+    method: 'post',
+    data: data
+  })
+}

+ 17 - 0
src/api/base/tasks.js

@@ -9,6 +9,23 @@ export function listTasks(query) {
   })
 }
 
+// 查询农事任务统计
+export function listTasksStatistics(query) {
+  return request({
+    url: '/base/tasks/listStatistics',
+    method: 'get',
+    params: query
+  })
+}
+
+
+export function listTasksTodoData() {
+  return request({
+    url: '/base/tasks/listTasksTodoData',
+    method: 'get',
+  })
+}
+
 // 查询农事任务详细
 export function getTasks(id) {
   return request({

+ 112 - 252
src/components/Map/index.vue

@@ -1,293 +1,153 @@
 <template>
- <div >
-   <!-- <el-row>
+  <div>
+    <!-- 地图容器 -->
+    <div id="map-container" style="width: 100%; height: 1300px;"></div>
 
-     <Button @click="drawFence">添加区域</Button>
-     <Button @click="drawFence">清除区域</Button>
-     <Button @click="saveRectangle">保存区域</Button>
-   </el-row> -->
-
-   <el-row style="height: 1300px;width: 100%;">
-     <el-amap class="amap-box" vid="map"
-              :zoom="zoom"
-              :center="center"
-              :amap-manager="amapManager"
-              :events="events"
-     >
-       <el-amap-marker v-for="(u,i) in markers" :position="u.position" :key="i" :icon="icon">
-       </el-amap-marker>
-
-     </el-amap>
-   </el-row>
+    <!-- 工具栏 -->
     <el-row type="flex" justify="end" class="m_tools">
-      <el-col  style="width:auto;">
+      <el-col style="width: auto;">
         <el-button-group size="small">
-          <el-button type="default" title="绘制围栏" size="small"   v-if="tools_show.place_show" v-show="!tools_show.oc_show" icon="el-icon-edit" @click="drawFence"></el-button>
-          <el-button type="default" title="保存围栏" size="small"  v-if="tools_show.place_show" v-show="!tools_show.oc_show" icon="el-icon-check" @click="saveRectangle" ></el-button>
-          <el-button type="default" title="清除" size="small"v-if="tools_show.place_show" v-show="!tools_show.oc_show" icon="el-icon-delete" @click="delemap"></el-button>
-          <el-button type="default" title="展开/收起" size="small" v-if="tools_show.openclose_show" :icon="tools_show.oc_show?'el-icon-d-arrow-left':'el-icon-d-arrow-right'" @click="tools_show.oc_show=!tools_show.oc_show"></el-button>
+          <el-button
+            type="default"
+            title="绘制围栏"
+            size="small"
+            v-if="tools_show.place_show"
+            v-show="!tools_show.oc_show"
+            icon="el-icon-edit"
+            @click="drawFence"
+          ></el-button>
+          <el-button
+            type="default"
+            title="保存围栏"
+            size="small"
+            v-if="tools_show.place_show"
+            v-show="!tools_show.oc_show"
+            icon="el-icon-check"
+            @click="saveRectangle"
+          ></el-button>
+          <el-button
+            type="default"
+            title="清除"
+            size="small"
+            v-if="tools_show.place_show"
+            v-show="!tools_show.oc_show"
+            icon="el-icon-delete"
+            @click="clearMap"
+          ></el-button>
+          <el-button
+            type="default"
+            title="展开/收起"
+            size="small"
+            v-if="tools_show.openclose_show"
+            :icon="tools_show.oc_show ? 'el-icon-d-arrow-left' : 'el-icon-d-arrow-right'"
+            @click="tools_show.oc_show = !tools_show.oc_show"
+          ></el-button>
         </el-button-group>
       </el-col>
-
     </el-row>
-
   </div>
-
-
 </template>
 
 <script>
-import VueAMap from 'vue-amap'
-
-let amapManager = new VueAMap.AMapManager()
-window._AMapSecurityConfig = {
-  securityJsCode: 'b672db78212bb58899d609f0ce9f2d97'
-}
-
 export default {
   name: "Map",
   data() {
     return {
-      // 地图对象
-      amapManager,
-      zoom: 12,
-      center: [116.404269,39.916042],
-      position: [121.5273285, 31.21515044],
-      events: {
-        init (o) {
-
-        }
-      },
-
-      icon: new AMap.Icon({
-        image: "https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png"
-      }),
-      searchOption: {
-        city: '北京',
-        citylimit: false
-      },
-      fenceForm:{
-        coordinate:[]
-      },
-      rectangle:null,
-      mouseTool: null,
-      overlays: [],
       map: null,
-      markers: [//标记点位置
-        
-      ],
-      formData: {
-        carId: '',
-        pageNum: 1,//当前页
-        pageSize: 10,//页长
-        pageTotal: 0,//总数
-      },
-      map: null,
-      path: [],//以前绘制的数据
-      paths: [], // 当前绘制的多边形经纬度数组
-      polygonItems: [], // 地图上绘制的所有多边形对象
-      polyEditors: [],// 新增数据=>所有编辑对象数组
-      polyEditorsBefore: [],// 以前历史数据=>进入编辑对象数组
-
-      // 总条数
+      mouseTool: null,
+      polygon: null,
+      paths: [],
       tools_show: {
-        place_show:true,
-        openclose_show:true,
-        oc_show:false
-      },
-
-      // 查询参数
-      queryParams: {
-        pageNum: 1,
-        pageSize: 10,
-        fenceName: null,
-        deptId: null,
+        place_show: true,
+        openclose_show: true,
+        oc_show: false,
       },
-      // 表单参数
-      form: {}
     };
-  },
-  watch: {
-
   },
   mounted() {
-    //DOM初始化完成进行地图初始化
-    //this.initAMap()
+    this.initMap();
   },
   methods: {
-    // 绘制多边形
-    drawFence() {
-      let _this = this
-      this.map = this.amapManager.getMap()
-      let map = this.amapManager.getMap()
-      /* if(this.fenceForm.coordinate.length >0){
-         this.$Message.warning("围栏已存在!")
-         return
-       }*/
-      if(this.rectangle){
-        map.remove(this.rectangle)
-      }
-      map.plugin(['AMap.MouseTool'], function () {
-        let mouseTool = new AMap.MouseTool(map)
-        _this.mouseTool = mouseTool
-        //添加事件
-        /*mouseTool.rectangle()
-        AMap.event.addListener(mouseTool, 'draw', function (e) {
-          _this.fenceForm.coordinate = []
-          let path = e.obj.getPath()
-          path.forEach(e=>{
-            _this.fenceForm.coordinate.push([e.getLng(),e.getLat()])
-          })
-          mouseTool.close(false)
-        })*/
-        const polygon = mouseTool.polygon({
-          //polygon:绘制多边形【线段:polyline;矩形:rectangle;圆:circle】
-          strokeColor: 'red',
-          strokeOpacity: 0.4,
-          strokeWeight: 2,//线宽
-          fillColor: '#1791fc', //填充色
-          fillOpacity: 0.2,//填充透明度
-          // strokeStyle还支持 solid
-          strokeStyle: 'solid',
-          // strokeDasharray: [30,10],
+    // 初始化地图
+    initMap() {
+      // 动态加载高德地图 SDK
+      const script = document.createElement("script");
+      script.src =
+        "https://webapi.amap.com/maps?v=1.4.15&key=bee95ab1796ad6803492c1bf9da311ab";
+      script.onload = () => {
+        this.map = new AMap.Map("map-container", {
+          zoom: 12,
+          center: [116.404269, 39.916042],
         });
-        mouseTool.on('draw', function (event) {
-          // event.obj 为绘制出来的覆盖物对象
-          let polygonItem = event.obj;
-          let paths = polygonItem.getPath();//取得绘制的多边形的每一个点坐标
-          // console.log('覆盖物对象绘制完成各个点的坐标', paths, event);
-
-          let path_in = [];  // 编辑的路径
-          paths.forEach(v => {
-            path_in.push([v.lng, v.lat])
-            _this.fenceForm.coordinate.push([v.lng, v.lat])
-          });
-          _this.paths = path_in //将新增数据放入paths数组里
-          _this.path = path_in //将新增数据放入paths数组里
 
-          //this.editRectangle();//绘制完成,默认进入编辑状态
-          var polygonItems=[];
-           polygonItems .push(event.obj);
-          //this.map.remove(event.obj); // 删除多边形
-           console.log(_this.paths, '------polygon-----');
+        // 添加插件
+        AMap.plugin(["AMap.MouseTool"], () => {
+          this.mouseTool = new AMap.MouseTool(this.map);
         });
-      })
+      };
+      document.head.appendChild(script);
     },
-    // 编辑围栏
-    editRectangle() {
-      const path = this.paths;
-      //新增的进入编辑状态
-      let polygon = new AMap.Polygon({
-        path: path,
-        strokeColor: "#FF33FF",
-        strokeWeight: 6,
-        strokeOpacity: 0.2,
-        fillOpacity: 0.2,
-        fillColor: '#1791fc',
-        zIndex: 50,
-      });
-      this.map.add(polygon);
-      this.polygonItem.push(polygon);
-      // 缩放地图到合适的视野级别
-      this.map.setFitView([polygon]);
 
-      this.polyEditor = new AMap.PolyEditor(this.map, polygon);
-      this.polyEditor.open();
-      this.polyEditors.push(this.polyEditor);
+    // 绘制多边形
+    drawFence() {
+      if (this.polygon) {
+        this.map.remove(this.polygon);
+      }
 
-      //历史围栏的进入编辑状态
-      let polygonBefore = new AMap.Polygon({
-        path: this.path,
-        strokeColor: "#FF33FF",
-        strokeWeight: 6,
-        strokeOpacity: 0.2,
+      this.mouseTool.polygon({
+        strokeColor: "red",
+        strokeOpacity: 0.4,
+        strokeWeight: 2,
+        fillColor: "#1791fc",
         fillOpacity: 0.2,
-        fillColor: '#1791fc',
-        zIndex: 50,
       });
-      this.map.add(polygonBefore);
-      this.polygonItem.push(polygonBefore);
-      // 缩放地图到合适的视野级别
-      this.map.setFitView([polygonBefore]);
-
-      this.polyEditorBefore = new AMap.PolyEditor(this.map, polygonBefore);
-      this.polyEditorBefore.open();
-      this.polyEditorsBefore.push(this.polyEditorBefore);
-
-      // this.polyEditor.on('addnode', function (event) {
-      //     console.info('触发事件:addnode', event)
-      //     console.info('修改后的经纬度:', polygon.getPath())
-      // });
-
-      // this.polyEditor.on('adjust', function (event) {
-      //     console.info('触发事件:adjust', event)
-      //     console.info('修改后的经纬度:', polygon.getPath())
-      // });
-
-      // this.polyEditor.on('removenode', function (event) {
-      //     console.info('触发事件:removenode', event)
-      //     console.info('修改后的经纬度:', polygon.getPath())
-      // });
 
-      // this.polyEditor.on('end', function (event) {
-      //     console.info('触发事件: end', event)
-      //     console.info('end修改后的经纬度:', polygon.getPath())
-      //     // event.target 即为编辑后的多边形对象
-      // });
-    },
-    // 取消编辑状态
-    cancelRectangle() {
-      this.polyEditors.forEach(item => { item.close(); });//新增
-      this.polyEditorsBefore.forEach(item => { item.close(); });//历史
+      this.mouseTool.on("draw", (event) => {
+        this.polygon = event.obj;
+        this.paths = this.polygon.getPath().map((point) => [point.lng, point.lat]);
+        console.log("绘制完成的路径:", this.paths);
+      });
     },
-    //保存围栏
+
+    // 保存围栏
     saveRectangle() {
-      console.log(this.path,'---save----');
-      //保存更新围栏经纬度
-      let polygon="";
-      var polygon_start="POLYGON((";
-        this.path.forEach(v => {
-          polygon=polygon+v[0]+" "+v[1]+",";
-        });
+      if (!this.paths.length) {
+        this.$message.warning("请先绘制围栏!");
+        return;
+      }
 
-      let result = polygon_start+polygon.slice(0, polygon.length - 1)+","+polygon.split(",")[0]+"))";
-      let id = this.$route.params.fenceId
-      let fencePolygon={fenceId:id,fencePolygon:result};
-      
+      const polygonStr = this.paths
+        .map((point) => point.join(" "))
+        .join(",");
+      const result = `POLYGON((${polygonStr}))`;
+      console.log("保存的围栏数据:", result);
 
-    },
-    // 删除围栏
-    deleRectangle() {
-      this.map.clearMap(); // 删除地图所有覆盖物
-      //删除=>成功(重新刷新页面)
-    },
-    delemap() {
-      this.map.clearMap(); // 删除地图所有覆盖物
+      // 发送请求保存围栏(示例)
+      const id = this.$route.params.fenceId;
+      const fencePolygon = { fenceId: id, fencePolygon: result };
+      console.log("围栏对象:", fencePolygon);
     },
 
-  }
+    // 清除地图
+    clearMap() {
+      if (this.polygon) {
+        this.map.remove(this.polygon);
+        this.polygon = null;
+        this.paths = [];
+      }
+    },
+  },
 };
 </script>
 
-<style lang="scss" scoped>
-  ::v-deep .amap-logo {
-    display: none;
-    opacity: 0 !important;
-  }
-  ::v-deep .amap-copyright {
-    opacity: 0;
-  }
-</style>
-<style lang="scss" scoped>
-  .m_tools {
-    position: absolute;
-    z-index: 999;
-    // left: 550px;
-    // top: 120px;
-    left: 85%;
-    top: 1px;
-    width: auto;
-    color: #409eff;
-    overflow: hidden;
-  }
-</style>
+<style scoped>
+.m_tools {
+  position: absolute;
+  z-index: 999;
+  left: 85%;
+  top: 1px;
+  width: auto;
+  color: #409eff;
+  overflow: hidden;
+}
+</style>

+ 5 - 3
src/main.js

@@ -59,12 +59,14 @@ VueAMap.initAMapApiLoader({
     'AMap.Geolocation', // 定位控件,用来获取和展示用户主机所在的经纬度位置,
     'AMap.DistrictSearch',
     'AMap.MouseTool',
-    'Geocoder'
+    'Geocoder',
+    'AMap.HeatMap'  // 添加这一行
   ],
-  v: '1.4.15', // 默认高德 sdk 版本为 1.4.4
-  uiVersion: '1.0.11'
+  v: '2.0', // 默认高德 sdk 版本为 1.4.4
+  uiVersion: '2.0'
 })
 
+
 // 添加全局调试信息
 console.log('VueAMap 初始化完成')
 window.addEventListener('load', () => {

+ 1 - 1
src/permission.js

@@ -9,7 +9,7 @@ import { isRelogin } from '@/utils/request'
 
 NProgress.configure({ showSpinner: false })
 
-const whiteList = ['/login', '/register']
+const whiteList = ['/login', '/register','/map/map']
 
 const isWhiteList = (path) => {
   return whiteList.some(pattern => isPathMatch(pattern, path))

+ 936 - 0
src/views/base/GrapeDiseaseReport/index.vue

@@ -0,0 +1,936 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <!-- <el-form-item label="作物类型" prop="cropType">
+        <el-select v-model="queryParams.cropType" placeholder="请选择作物类型" clearable>
+          <el-option
+            v-for="dict in dict.type.crop_type"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item> -->
+      <el-form-item label="作物名称" prop="cropType">
+        <el-input v-model="queryParams.cropType" placeholder="请输入作物名称" clearable @keyup.enter.native="handleQuery" />
+      </el-form-item>
+      <el-form-item label="设备编号" prop="deviceCode">
+        <el-input v-model="queryParams.deviceCode" placeholder="请输入设备编号" clearable @keyup.enter.native="handleQuery" />
+      </el-form-item>
+
+      <el-form-item label="病害名称" prop="diseaseName">
+        <el-input v-model="queryParams.diseaseName" placeholder="请输入病害名称" clearable @keyup.enter.native="handleQuery" />
+      </el-form-item>
+
+      <!-- <el-form-item label="严重程度" prop="severityLevel">
+        <el-input
+          v-model="queryParams.severityLevel"
+          placeholder="请输入严重程度"
+          clearable
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item> -->
+      <el-form-item label="识别时间" prop="collectTime">
+        <el-date-picker clearable style="width:200px" v-model="queryParams.collectTime" type="date"
+          value-format="yyyy-MM-dd" placeholder="请选择采集/识别时间">
+        </el-date-picker>
+      </el-form-item>
+      <!-- <el-form-item label="所在农场" prop="farmName">
+        <el-input style="width:200px" v-model="queryParams.farmName" placeholder="请输入所在农场" clearable
+          @keyup.enter.native="handleQuery" />
+      </el-form-item> -->
+
+      <el-form-item label="所属农场" prop="deptIdList">
+        <treeselect v-model="queryParams.deptIdList" :options="deptOptions" multiple :flat="true" :limit="1"
+          :limitText="count => `+${count}`" style="width:200px" :show-count="true" placeholder="请选所属农场" />
+      </el-form-item>
+
+      <el-form-item label="处理状态" prop="handleStatus">
+        <el-select v-model="queryParams.handleStatus" placeholder="请选择处理状态" clearable>
+          <el-option v-for="dict in dict.type.ai_grape_disease_handle_status" :key="dict.value" :label="dict.label"
+            :value="dict.value" />
+        </el-select>
+      </el-form-item>
+
+
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 统计面板 -->
+    <div class="statistics-panel">
+      <el-row :gutter="12">
+        <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
+          <div class="stat-card total-card">
+            <div class="stat-content">
+              <div class="stat-title">今日识别总数</div>
+              <div class="stat-number">{{ statistics.todayCount || 0 }}</div>
+              <div class="stat-desc">识别数量</div>
+            </div>
+          </div>
+        </el-col>
+        <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
+          <div class="stat-card tech-card">
+            <div class="stat-content">
+              <div class="stat-title">待处理建议数</div>
+              <div class="stat-number">{{ statistics.pendingCount || 0 }}</div>
+              <div class="stat-desc">建议数量</div>
+            </div>
+          </div>
+        </el-col>
+        <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
+        </el-col>
+        <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
+        </el-col>
+        <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
+        </el-col>
+      </el-row>
+    </div>
+
+    <el-row :gutter="10" class="mb8">
+
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
+          v-hasPermi="['base:GrapeDiseaseReport:export']">导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="GrapeDiseaseReportList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="所属农场" align="center" key="farmName" prop="farmName" />
+      <el-table-column label="作物名称" align="center" prop="cropType" />
+      <el-table-column label="设备编号" align="center" prop="deviceCode" />
+      <el-table-column label="经度" align="center" prop="lng" />
+      <el-table-column label="纬度" align="center" prop="lat" />
+      <el-table-column label="病害名称" align="center" prop="diseaseName" />
+      <el-table-column label="置信度" align="center" prop="confidence" />
+      <!-- <el-table-column label="严重程度" align="center" prop="severityLevel">
+        <template slot-scope="scope">
+          <dict-tag :options="dict.type.ai_grape_disease_report_severity_level" :value="scope.row.severityLevel"/>
+        </template>
+</el-table-column> -->
+      <el-table-column label="识别时间" align="center" prop="collectTime" width="200">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.collectTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="处理状态" align="center" prop="handleStatus">
+        <template slot-scope="scope">
+          <dict-tag :options="dict.type.ai_grape_disease_handle_status" :value="scope.row.handleStatus" />
+        </template>
+      </el-table-column>
+
+
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)"
+            v-hasPermi="['base:GrapeDiseaseReport:view']">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
+      @pagination="getList" />
+
+
+
+    <!-- 详情对话框 -->
+    <el-dialog :visible.sync="detailOpen" width="60%" append-to-body class="detail-dialog">
+      <div v-if="detailData" class="report-detail-content">
+        <!-- 基础信息区 -->
+        <section class="info-section">
+          <h3>基础信息</h3>
+          <div class="info-grid">
+            <div class="info-item">
+              <label>设备编号:</label>
+              <span>{{ detailData.deviceCode }}</span>
+            </div>
+            <div class="info-item">
+              <label>采集位置:</label>
+              <span>经度: {{ detailData.lng }},纬度: {{ detailData.lat }}</span>
+            </div>
+            <div class="info-item">
+              <label>识别时间:</label>
+              <span>{{ parseTime(detailData.collectTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+            </div>
+            <div class="info-item">
+              <label>所在农场:</label>
+              <span>{{ detailData.farmName || '未填写' }}</span>
+            </div>
+          </div>
+        </section>
+
+        <!-- 识别结果与图片区 -->
+        <section class="result-image-section">
+          <h3>识别结果与图片</h3>
+          <div class="result-image-container">
+            <!-- 左侧图片 -->
+            <div class="image-area">
+              <!-- <h4>病害图片</h4> -->
+              <div class="image-container">
+                <el-image v-if="detailData.imgUrl" :src="detailData.imgUrl" :preview-src-list="[detailData.imgUrl]"
+                  fit="contain" style="max-width: 300px; max-height: 300px;">
+                </el-image>
+                <p v-else>暂无图片</p>
+              </div>
+            </div>
+
+            <!-- 右侧识别结果 -->
+            <div class="result-area">
+              <h4>识别结果</h4>
+              <div class="result-details">
+                <div class="result-item">
+                  <label>病害名称:</label>
+                  <span>{{ detailData.diseaseName }}</span>
+                </div>
+                <div class="result-item">
+                  <label>置信度:</label>
+                  <span>{{ detailData.confidence }}%</span>
+                </div>
+                <!-- <div class="result-item">
+              <label>严重程度:</label>
+              <span>
+                <dict-tag 
+                  :options="dict.type.ai_grape_disease_report_severity_level" 
+                  :value="detailData.severityLevel"/>
+              </span>
+            </div> -->
+                <!-- <div class="result-item">
+              <label>发病原因分析:</label>
+              <span>AI智能分析结果</span>
+            </div> -->
+              </div>
+            </div>
+          </div>
+        </section>
+
+        <!-- 农事建议区 -->
+        <section class="advice-section">
+          <h3>农事建议</h3>
+          <div class="advice-list">
+            <div class="advice-item">
+              <h4>紧急处理措施</h4>
+              <p>{{ detailData.emergencyMeasure || '暂无建议' }}</p>
+            </div>
+            <div class="advice-item">
+              <h4>田间管理建议</h4>
+              <p>{{ detailData.manageAdvice || '暂无建议' }}</p>
+            </div>
+            <div class="advice-item">
+              <h4>预防方案</h4>
+              <p>{{ detailData.preventPlan || '暂无建议' }}</p>
+            </div>
+          </div>
+        </section>
+
+        <!-- 操作区 -->
+        <section class="action-section">
+          <div class="action-buttons">
+            <el-button type="primary" @click="handleMarkProcessed" :disabled="detailData.handleStatus === 1">
+              标记已处理
+            </el-button>
+            <el-button @click="closeDetailDialog">关闭</el-button>
+          </div>
+        </section>
+      </div>
+    </el-dialog>
+
+    <!-- 添加或修改AI农作物病害诊断报告对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+
+        <el-form-item label="作物类型" prop="cropType">
+          <el-select v-model="form.cropType" placeholder="请选择作物类型">
+            <el-option v-for="dict in dict.type.crop_type" :key="dict.value" :label="dict.label"
+              :value="dict.value"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="设备编号" prop="deviceCode">
+          <el-input v-model="form.deviceCode" placeholder="请输入设备编号" />
+        </el-form-item>
+        <el-form-item label="经度" prop="lng">
+          <el-input v-model="form.lng" placeholder="请输入经度" />
+        </el-form-item>
+        <el-form-item label="纬度" prop="lat">
+          <el-input v-model="form.lat" placeholder="请输入纬度" />
+        </el-form-item>
+        <el-form-item label="病害名称" prop="diseaseName">
+          <el-input v-model="form.diseaseName" placeholder="请输入病害名称" />
+        </el-form-item>
+        <el-form-item label="AI识别置信度" prop="confidence">
+          <el-input v-model="form.confidence" placeholder="请输入AI识别置信度" />
+        </el-form-item>
+        <el-form-item label="严重程度" prop="severityLevel">
+          <el-input v-model="form.severityLevel" placeholder="请输入严重程度" />
+        </el-form-item>
+        <el-form-item label="采集/识别时间" prop="collectTime">
+          <el-date-picker clearable v-model="form.collectTime" type="date" value-format="yyyy-MM-dd"
+            placeholder="请选择采集/识别时间">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="所在地块/农场" prop="farmName">
+          <el-input v-model="form.farmName" placeholder="请输入所在地块/农场" />
+        </el-form-item>
+        <el-form-item label="病害图片访问URL" prop="imgUrl">
+          <el-input v-model="form.imgUrl" placeholder="请输入病害图片访问URL" />
+        </el-form-item>
+        <el-form-item label="处理状态" prop="handleStatus">
+          <el-radio-group v-model="form.handleStatus">
+            <el-radio v-for="dict in dict.type.ai_grape_disease_handle_status" :key="dict.value"
+              :label="parseInt(dict.value)">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="紧急处理措施" prop="emergencyMeasure">
+          <el-input v-model="form.emergencyMeasure" type="textarea" placeholder="请输入内容" />
+        </el-form-item>
+        <el-form-item label="田间管理建议" prop="manageAdvice">
+          <el-input v-model="form.manageAdvice" type="textarea" placeholder="请输入内容" />
+        </el-form-item>
+        <el-form-item label="预防方案" prop="preventPlan">
+          <el-input v-model="form.preventPlan" type="textarea" placeholder="请输入内容" />
+        </el-form-item>
+        <el-form-item label="处理备注" prop="handleNote">
+          <el-input v-model="form.handleNote" placeholder="请输入处理备注" />
+        </el-form-item>
+        <el-form-item label="处理人" prop="handleUser">
+          <el-input v-model="form.handleUser" placeholder="请输入处理人" />
+        </el-form-item>
+        <el-form-item label="处理时间" prop="handleTime">
+          <el-date-picker clearable v-model="form.handleTime" type="date" value-format="yyyy-MM-dd"
+            placeholder="请选择处理时间">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="创建时间" prop="createdTime">
+          <el-date-picker clearable v-model="form.createdTime" type="date" value-format="yyyy-MM-dd"
+            placeholder="请选择创建时间">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="更新时间" prop="updatedTime">
+          <el-date-picker clearable v-model="form.updatedTime" type="date" value-format="yyyy-MM-dd"
+            placeholder="请选择更新时间">
+          </el-date-picker>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listGrapeDiseaseReport, getGrapeDiseaseReport, delGrapeDiseaseReport, addGrapeDiseaseReport, updateGrapeDiseaseReport } from "@/api/base/GrapeDiseaseReport"
+import { deptTreeSelect } from "@/api/system/user";
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+export default {
+  name: "GrapeDiseaseReport",
+  components: { Treeselect },
+  dicts: ['ai_grape_disease_handle_status', 'crop_type'],
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // AI农作物病害诊断报告表格数据
+      GrapeDiseaseReportList: [],
+      // 部门树选项
+      deptOptions: undefined,
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 详情弹窗相关
+      detailOpen: false,
+      detailData: {},
+
+      // 统计数据
+      statistics: {
+        todayCount: 0,
+        pendingCount: 0,
+
+      },
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        cropType: null,
+        deviceCode: null,
+        lng: null,
+        lat: null,
+        diseaseName: null,
+        confidence: null,
+        severityLevel: null,
+        collectTime: null,
+        farmName: null,
+        imgUrl: null,
+        handleStatus: null,
+        emergencyMeasure: null,
+        manageAdvice: null,
+        preventPlan: null,
+        handleNote: null,
+        handleUser: null,
+        handleTime: null,
+        createdTime: null,
+        updatedTime: null,
+        deptIdList: [],
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        cropType: [
+          { required: true, message: "作物类型不能为空", trigger: "change" }
+        ],
+        deviceCode: [
+          { required: true, message: "设备编号不能为空", trigger: "blur" }
+        ],
+        lng: [
+          { required: true, message: "经度不能为空", trigger: "blur" }
+        ],
+        lat: [
+          { required: true, message: "纬度不能为空", trigger: "blur" }
+        ],
+        diseaseName: [
+          { required: true, message: "病害名称不能为空", trigger: "blur" }
+        ],
+        confidence: [
+          { required: true, message: "AI识别置信度不能为空", trigger: "blur" }
+        ],
+        severityLevel: [
+          { required: true, message: "严重程度不能为空", trigger: "blur" }
+        ],
+        collectTime: [
+          { required: true, message: "采集/识别时间不能为空", trigger: "blur" }
+        ],
+        imgUrl: [
+          { required: true, message: "病害图片访问URL不能为空", trigger: "blur" }
+        ],
+        handleStatus: [
+          { required: true, message: "处理状态不能为空", trigger: "change" }
+        ],
+        emergencyMeasure: [
+          { required: true, message: "紧急处理措施不能为空", trigger: "blur" }
+        ],
+        manageAdvice: [
+          { required: true, message: "田间管理建议不能为空", trigger: "blur" }
+        ],
+        preventPlan: [
+          { required: true, message: "预防方案不能为空", trigger: "blur" }
+        ],
+      }
+    }
+  },
+  created() {
+    this.getList()
+    this.getDeptTree();
+    this.getStatistics()
+  },
+  methods: {
+
+/** 获取统计数据 - 兼容:待处理数=全部历史数据 */
+getStatistics() {
+  const todayStr = this.parseTime(new Date(), '{y}-{m}-{d}');
+  console.log('todayStr', todayStr)
+  listGrapeDiseaseReport().then(response => {
+    const data = response?.rows || [];
+    let todayCount = 0;
+    let pendingCount = 0;
+
+    data.forEach(item => {
+      // 统计:全部数据的待处理数量
+      if (item.handleStatus === 0) {
+        pendingCount++;
+      }
+      // 统计:今日数据数量
+      if (item.collectTime) {
+        const itemDateStr = this.parseTime(item.collectTime, '{y}-{m}-{d}');
+        console.log('itemDateStr', itemDateStr)
+        if (itemDateStr === todayStr) {
+          todayCount++;
+        }
+      }
+    });
+
+    this.statistics = { todayCount, pendingCount };
+  }).catch(() => {
+    this.statistics = { todayCount: 0, pendingCount: 0 };
+  });
+},
+
+    /** 查询部门下拉树结构 */
+    getDeptTree() {
+      deptTreeSelect().then(response => {
+        this.deptOptions = response.data;
+      });
+    },
+    /** 查询AI农作物病害诊断报告列表 */
+    getList() {
+      this.loading = true
+      listGrapeDiseaseReport(this.queryParams).then(response => {
+        this.GrapeDiseaseReportList = response.rows
+        this.total = response.total
+        this.loading = false
+      })
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false
+      this.reset()
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        cropType: null,
+        deviceCode: null,
+        lng: null,
+        lat: null,
+        diseaseName: null,
+        confidence: null,
+        severityLevel: null,
+        collectTime: null,
+        farmName: null,
+        imgUrl: null,
+        handleStatus: null,
+        emergencyMeasure: null,
+        manageAdvice: null,
+        preventPlan: null,
+        handleNote: null,
+        handleUser: null,
+        handleTime: null,
+        createdTime: null,
+        updatedTime: null
+      }
+      this.resetForm("form")
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm")
+      this.handleQuery()
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset()
+      this.open = true
+      this.title = "添加AI农作物病害诊断报告"
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset()
+      const id = row.id || this.ids
+      getGrapeDiseaseReport(id).then(response => {
+        this.form = response.data
+        this.open = true
+        this.title = "修改AI农作物病害诊断报告"
+      })
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateGrapeDiseaseReport(this.form).then(response => {
+              this.$modal.msgSuccess("修改成功")
+              this.open = false
+              this.getList()
+            })
+          } else {
+            addGrapeDiseaseReport(this.form).then(response => {
+              this.$modal.msgSuccess("新增成功")
+              this.open = false
+              this.getList()
+            })
+          }
+        }
+      })
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids
+      this.$modal.confirm('是否确认删除AI农作物病害诊断报告编号为"' + ids + '"的数据项?').then(function () {
+        return delGrapeDiseaseReport(ids)
+      }).then(() => {
+        this.getList()
+        this.$modal.msgSuccess("删除成功")
+      }).catch(() => { })
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      this.download('base/GrapeDiseaseReport/export', {
+        ...this.queryParams
+      }, `GrapeDiseaseReport_${new Date().getTime()}.xlsx`)
+    },
+
+    /** 查看详情按钮操作 */
+    handleView(row) {
+      const id = row.id
+      getGrapeDiseaseReport(id).then(response => {
+        this.detailData = response.data
+        this.detailOpen = true
+      })
+    },
+
+    /** 标记已处理按钮 */
+    handleMarkProcessed() {
+      this.$prompt('请输入处理备注', '标记已处理', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        inputPattern: /\S/,
+        inputErrorMessage: '处理备注不能为空'
+      }).then(({ value }) => {
+        const processedData = {
+          id: this.detailData.id,
+          handleStatus: 1, // 设置为已处理
+          handleNote: value,
+          handleUser: this.$store.getters.name, // 获取当前登录用户
+          handleTime: new Date().toISOString().slice(0, 19).replace('T', ' ') // 当前时间
+        }
+
+        updateGrapeDiseaseReport(processedData).then(response => {
+          this.$modal.msgSuccess("标记已处理成功")
+          this.detailOpen = false
+          this.getList() // 刷新列表
+        })
+      }).catch(() => {
+        // 用户取消操作
+      })
+    },
+
+    // 关闭详情弹窗
+    closeDetailDialog() {
+      this.detailOpen = false
+    }
+  }
+}
+</script>
+<style lang="scss">
+.report-detail-content {
+
+  .info-section,
+  .result-image-section,
+  .advice-section,
+  .action-section {
+    margin-bottom: 24px;
+    padding: 16px;
+    border: 1px solid #ebeef5;
+    border-radius: 4px;
+    background-color: #fafafa;
+
+    h3 {
+      margin-top: 0;
+      margin-bottom: 16px;
+      font-size: 16px;
+      color: #303133;
+      border-bottom: 1px solid #dcdfe6;
+      padding-bottom: 8px;
+    }
+
+    h4 {
+      margin: 8px 0;
+      color: #606266;
+      font-weight: bold;
+    }
+  }
+
+  .info-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+    gap: 16px;
+
+    .info-item {
+      display: flex;
+      flex-direction: column;
+
+      label {
+        font-weight: bold;
+        color: #909399;
+        font-size: 14px;
+        margin-bottom: 4px;
+      }
+
+      span {
+        color: #606266;
+      }
+    }
+  }
+
+  .result-image-container {
+    display: flex;
+    gap: 20px;
+
+    .image-area,
+    .result-area {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+
+      .image-container {
+        text-align: center;
+        padding: 16px 0;
+      }
+
+      .result-details {
+        display: flex;
+        flex-direction: column;
+        gap: 12px;
+
+        .result-item {
+          display: flex;
+          flex-direction: column;
+
+          label {
+            font-weight: bold;
+            color: #909399;
+            font-size: 14px;
+            margin-bottom: 4px;
+          }
+
+          span {
+            color: #606266;
+          }
+        }
+      }
+    }
+  }
+
+  .advice-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+    gap: 16px;
+
+    .advice-item {
+      background-color: white;
+      padding: 16px;
+      border-radius: 4px;
+
+      p {
+        margin: 0;
+        line-height: 1.6;
+        color: #606266;
+      }
+    }
+  }
+
+  .action-section {
+    text-align: right;
+    background: none;
+    border: none;
+    padding: 0;
+  }
+
+  .advice-list {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+
+    .advice-item {
+      background-color: white;
+      padding: 16px;
+      border-radius: 4px;
+      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+
+      h4 {
+        margin: 0 0 12px 0;
+        color: #606266;
+        font-weight: bold;
+        font-size: 14px;
+        border-bottom: 1px solid #ebeef5;
+        padding-bottom: 8px;
+      }
+
+      p {
+        margin: 0;
+        line-height: 1.6;
+        color: #606266;
+        word-wrap: break-word;
+        white-space: pre-wrap;
+        /* 保留换行符 */
+      }
+    }
+  }
+}
+
+.detail-dialog {
+  .el-dialog__body {
+    padding: 20px;
+  }
+}
+
+/* 统计面板样式 */
+.statistics-panel {
+  margin-bottom: 16px;
+  padding: 20px;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+}
+
+.statistics-panel .el-row {
+  display: flex;
+  justify-content: space-between;
+}
+
+.statistics-panel .el-col {
+  flex: 1;
+  max-width: none !important;
+}
+
+.stat-card {
+  border-radius: 12px;
+  padding: 16px;
+  height: 120px;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  transition: all 0.3s ease;
+  cursor: pointer;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.05);
+  position: relative;
+  overflow: hidden;
+  color: #fff;
+  margin-bottom: 10px;
+}
+
+.stat-card:hover {
+  transform: translateY(-3px);
+  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15), 0 4px 10px rgba(0, 0, 0, 0.1);
+}
+
+
+.stat-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: flex-start;
+}
+
+.stat-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: rgba(255, 255, 255, 0.9);
+  margin-bottom: 0;
+  line-height: 1.2;
+}
+
+.stat-number {
+  font-size: 32px;
+  font-weight: 700;
+  line-height: 1.0;
+  margin: 6px 0;
+  color: #fff;
+}
+
+.stat-desc {
+  font-size: 11px;
+  font-weight: 500;
+  color: rgba(255, 255, 255, 0.85);
+  line-height: 1.2;
+  margin: 0;
+}
+
+/* 不同卡片的主题色 */
+.total-card {
+  background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+}
+
+.tech-card {
+  background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
+}
+
+.policy-card {
+  background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
+}
+
+.published-card {
+  background: linear-gradient(135deg, #e60575 0%, #e61581 100%);
+}
+
+.draft-card {
+  background: linear-gradient(135deg, #06b6d4 0%, #38bdf8 100%);
+}
+
+.formatted-content {
+  line-height: 1.6;
+  color: #606266;
+
+  /* 为可能生成的标题设置样式 */
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6 {
+    margin: 12px 0 8px 0;
+    font-weight: bold;
+    color: #303133;
+  }
+
+  /* 为可能生成的段落设置样式 */
+  p {
+    margin: 8px 0;
+  }
+
+  /* 为可能生成的列表设置样式 */
+  ul,
+  ol {
+    margin: 8px 0;
+    padding-left: 20px;
+  }
+
+  li {
+    margin: 4px 0;
+  }
+
+  /* 为可能生成的粗体和斜体设置样式 */
+  strong,
+  b {
+    font-weight: bold;
+  }
+
+  em,
+  i {
+    font-style: italic;
+  }
+
+  /* 为可能生成的代码块设置样式 */
+  code {
+    background-color: #f5f5f5;
+    padding: 2px 4px;
+    border-radius: 3px;
+    font-family: monospace;
+  }
+
+  pre {
+    background-color: #f5f5f5;
+    padding: 10px;
+    border-radius: 4px;
+    overflow-x: auto;
+  }
+}
+</style>

+ 1023 - 0
src/views/base/GrapeDiseaseReport/map.vue

@@ -0,0 +1,1023 @@
+<template>
+  <div class="amap-container">
+    <div class="control-panel">
+      <button class="heatmap-toggle-btn" @click="toggleHeatmap" :class="{ active: heatmapVisible }">
+        {{ heatmapVisible ? '隐藏热力图' : '显示热力图' }}
+      </button>
+      <!-- 新增:切换多边形显示/隐藏按钮 -->
+      <button class="polygon-toggle-btn" @click="togglePolygon" :class="{ active: polygonVisible }">
+        {{ polygonVisible ? '隐藏危险等级' : '显示危险等级' }}
+      </button>
+    </div>
+    <div id="container" class="map-box"></div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'AmapHeatMap',
+  data() {
+    return {
+      map: null,
+      heatmap: null,
+      borderPolygon: null,
+      isMapLoaded: false,
+      borderColor: '#3dfcfc', // 默认青色
+      isAlarm: false,
+      animationInterval: null,
+      heatmapVisible: false,  // 热力图默认为关闭状态
+      polygonVisible: false,   // 多边形默认为显示状态
+      polygons: [],
+      infoWindow: null // 高德地图信息窗口
+    };
+  },
+  mounted() {
+    this.initMap();
+  },
+  beforeDestroy() {
+    // 清理动画
+    this.stopBorderAnimation();
+
+    // 移除信息窗口
+    if (this.infoWindow) {
+      this.infoWindow.close();
+      this.infoWindow = null;
+    }
+
+    // 优化销毁逻辑:先移除热力图层和边框,再销毁地图,防止内存泄漏
+    if (this.heatmap && this.map) {
+      this.map.remove(this.heatmap);
+    }
+    if (this.borderPolygon && this.map) {
+      this.map.remove(this.borderPolygon);
+    }
+    if (this.polygons && this.polygons.length > 0) {
+      this.polygons.forEach(polygon => {
+        this.map.remove(polygon);
+      });
+      this.polygons = [];
+    }
+    if (this.map) {
+      this.map.destroy();
+      this.map = null;
+      this.heatmap = null;
+      this.borderPolygon = null;
+      this.polygons = null;
+    }
+  },
+  methods: {
+
+
+    /**
+   * 获取所有定义的区域配置
+   * 可以在这里轻松添加新区域
+   */
+    getAreaConfigurations() {
+      return [
+        {
+          id: 'main',
+          name: '主要区域',
+          westLat: 116.989366,
+          eastLat: 116.990562,
+          northLng: 34.20432,
+          southLng: 34.203846,
+          cols: 18,
+          rows: 7
+        },
+        {
+          id: 'new',
+          name: '新区域',
+          westLat: 116.989367,
+          eastLat: 116.990559,
+          northLng: 34.204871,
+          southLng: 34.204353,
+          cols: 18,
+          rows: 7
+        },
+        {
+          id: 'third',
+          name: '第三区域',
+          westLat: 116.989373,
+          eastLat: 116.990562,
+          northLng: 34.203811,
+          southLng: 34.20335,
+          cols: 18,
+          rows: 7
+        },
+        // 最新添加的区域
+        {
+          id: 'fourth',
+          name: '第四区域',
+          westLat: 116.989373,
+          eastLat: 116.990572,
+          northLng: 34.203306,
+          southLng: 34.202892,
+          cols: 18,
+          rows: 7
+        },
+    // 再次添加的新区域
+    {
+      id: 'fifth',
+      name: '第五区域',
+      westLat: 116.989375,
+      eastLat: 116.990559,
+      northLng: 34.202867,
+      southLng: 34.202407,
+      cols: 18,
+      rows: 7
+    },
+    // 新增的第六个区域
+    {
+      id: 'sixth',
+      name: '第六区域',
+      westLat: 116.988406,
+      eastLat: 116.989257,
+      northLng: 34.204885,
+      southLng: 34.204355,
+      cols: 13,
+      rows: 7
+    },
+    // 新增的第七个区域
+    {
+      id: 'seventh',
+      name: '第七区域',
+      westLat: 116.9884,
+      eastLat: 116.989245,
+      northLng: 34.204324,
+      southLng: 34.203857,
+      cols: 13,  // 这个区域较窄,使用13列
+      rows: 7
+    }
+    ,
+    // 新增的第八个区域
+    {
+      id: 'eighth',
+      name: '第八区域',
+      westLat: 116.988396,
+      eastLat: 116.989241,
+      northLng: 34.203805,
+      southLng: 34.203354,
+      cols: 13,  // 这个区域较窄,使用13列
+      rows: 7
+    },
+    // 新增的第九个区域
+    {
+      id: 'ninth',
+      name: '第九区域',
+      westLat: 116.988392,
+      eastLat: 116.989234,
+      northLng: 34.203338,
+      southLng: 34.202895,
+      cols: 13,  // 这个区域较窄,使用13列
+      rows: 7
+    },
+    // 新增的第十个区域
+    {
+      id: 'tenth',
+      name: '第十区域',
+      westLat: 116.98838,
+      eastLat: 116.989232,
+      northLng: 34.202859,
+      southLng: 34.20242,
+      cols: 13,  // 这个区域较窄,使用13列
+      rows: 7
+    }
+        // 后续可以在这里继续添加新区域
+        // {
+        //   id: 'another_area',
+        //   name: '另一个区域',
+        //   westLat: 经度西,
+        //   eastLat: 经度东,
+        //   northLng: 纬度北,
+        //   southLng: 纬度南,
+        //   cols: 18,
+        //   rows: 7
+        // }
+      ];
+    },
+
+    /**
+     * 为单个区域生成网格
+     * @param {Object} area - 区域配置对象
+     * @param {number} baseId - ID起始值,避免重复
+     */
+    generateSingleAreaGrid(area, baseId = 0) {
+      const {
+        westLat, eastLat, northLng, southLng,
+        cols, rows, id: areaId, name: areaName
+      } = area;
+
+      const colWidth = (eastLat - westLat) / cols;
+      const rowHeight = (northLng - southLng) / rows;
+
+      // 风险等级数组
+      const riskLevels = ['healthy', 'low_risk', 'high_risk'];
+
+      const polygonsData = [];
+
+      for (let i = 0; i < rows; i++) {
+        for (let j = 0; j < cols; j++) {
+          // 计算当前小方块的四个角点坐标
+          const westCorner = westLat + j * colWidth;
+          const eastCorner = westLat + (j + 1) * colWidth;
+          const northCorner = northLng - i * rowHeight;
+          const southCorner = northLng - (i + 1) * rowHeight;
+
+          // 创建小方块的路径(顺时针方向)
+          const path = [
+            [westCorner, northCorner],     // 左上角
+            [eastCorner, northCorner],     // 右上角
+            [eastCorner, southCorner],     // 右下角
+            [westCorner, southCorner],     // 左下角
+            [westCorner, northCorner]      // 回到起点
+          ];
+
+          // 随机选择风险等级
+          const riskLevel = riskLevels[Math.floor(Math.random() * riskLevels.length)];
+
+          // 创建多边形数据
+          const polygonData = {
+            id: baseId + i * cols + j + 1,
+            name: `${areaName}-${i * cols + j + 1}`,
+            riskLevel: riskLevel,
+            path: path, // 注意:这里不需要嵌套数组
+            areaType: areaId,
+            areaName: areaName
+          };
+
+          polygonsData.push(polygonData);
+        }
+      }
+
+      return polygonsData;
+    },
+
+    /**
+ * 切换多边形显示/隐藏
+ */
+    /* togglePolygon() {
+      if (!this.polygons || this.polygons.length === 0) {
+        console.warn('多边形尚未初始化');
+        return;
+      }
+
+      if (this.polygonVisible) {
+        // 隐藏多边形
+        this.polygons.forEach(polygon => {
+          this.map.remove(polygon);
+        });
+        this.polygonVisible = false;
+        console.log('多边形已隐藏');
+      } else {
+        // 显示多边形
+        this.polygons.forEach(polygon => {
+          this.map.add(polygon);
+        });
+        this.polygonVisible = true;
+        console.log('多边形已显示');
+      }
+    }, */
+
+    /**
+   * 切换多边形显示/隐藏
+   */
+    togglePolygon() {
+      if (!this.polygons || this.polygons.length === 0) {
+        console.warn('多边形尚未初始化');
+        return;
+      }
+
+      if (this.polygonVisible) {
+        // 隐藏多边形
+        this.polygons.forEach(polygon => {
+          this.map.remove(polygon);
+        });
+        this.polygonVisible = false;
+        console.log('多边形已隐藏');
+      } else {
+        // 显示多边形
+        this.polygons.forEach(polygon => {
+          this.map.add(polygon);
+        });
+        this.polygonVisible = true;
+        console.log('多边形已显示');
+      }
+    },
+
+    // 修改 generateGridPolygons 方法,使其能够生成多个区域的网格
+    /* generateGridPolygons() {
+      // 主要区域坐标(原有的)
+      const mainWestLat = 116.989366;
+      const mainEastLat = 116.990562;
+      const mainNorthLng = 34.20432;
+      const mainSouthLng = 34.203846;
+    
+      // 新区域坐标
+      const newWestLat = 116.989367;
+      const newEastLat = 116.990559;
+      const newNorthLng = 34.204871;
+      const newSouthLng = 34.204353;
+    
+      // 定义区域配置
+      const areas = [
+        {
+          name: 'main',
+          westLat: mainWestLat,
+          eastLat: mainEastLat,
+          northLng: mainNorthLng,
+          southLng: mainSouthLng
+        },
+        {
+          name: 'new',
+          westLat: newWestLat,
+          eastLat: newEastLat,
+          northLng: newNorthLng,
+          southLng: newSouthLng
+        }
+      ];
+    
+      // 计算每列和每行的宽度和高度(对于两个区域都使用相同的行列数)
+      const cols = 18;
+      const rows = 7;
+    
+      // 风险等级数组
+      const riskLevels = ['healthy', 'low_risk', 'high_risk'];
+    
+      // 生成所有小方块的数据
+      const polygonsData = [];
+    
+      // 遍历每个区域
+      areas.forEach((area, areaIndex) => {
+        const colWidth = (area.eastLat - area.westLat) / cols;
+        const rowHeight = (area.northLng - area.southLng) / rows;
+    
+        for (let i = 0; i < rows; i++) {
+          for (let j = 0; j < cols; j++) {
+            // 计算当前小方块的四个角点坐标
+            const westCorner = area.westLat + j * colWidth;
+            const eastCorner = area.westLat + (j + 1) * colWidth;
+            const northCorner = area.northLng - i * rowHeight;
+            const southCorner = area.northLng - (i + 1) * rowHeight;
+    
+            // 创建小方块的路径(顺时针方向)
+            const path = [
+              [westCorner, northCorner],     // 左上角
+              [eastCorner, northCorner],     // 右上角
+              [eastCorner, southCorner],     // 右下角
+              [westCorner, southCorner],     // 左下角
+              [westCorner, northCorner]      // 回到起点
+            ];
+    
+            // 随机选择风险等级
+            const riskLevel = riskLevels[Math.floor(Math.random() * riskLevels.length)];
+    
+            // 创建多边形数据
+            const polygonData = {
+              id: areaIndex * cols * rows + i * cols + j + 1,
+              name: `${area.name}区域-${i * cols + j + 1}`,
+              riskLevel: riskLevel,
+              path: path, // 注意:这里不需要嵌套数组,因为createMultiplePolygons会自动处理
+              areaType: area.name
+            };
+    
+            polygonsData.push(polygonData);
+          }
+        }
+      });
+    
+      return polygonsData;
+    }, */
+
+    /**
+       * 生成所有区域的网格
+       */
+    generateGridPolygons() {
+      const areas = this.getAreaConfigurations();
+      let allPolygonsData = [];
+      let currentBaseId = 1;
+
+      areas.forEach(area => {
+        const areaPolygons = this.generateSingleAreaGrid(area, currentBaseId - 1);
+        allPolygonsData = allPolygonsData.concat(areaPolygons);
+
+        // 更新下一个区域的ID基数
+        currentBaseId += area.rows * area.cols;
+      });
+
+      return allPolygonsData;
+    },
+
+    /**
+     * 动态添加新区域
+     * @param {Object} newArea - 新区域配置
+     */
+    addNewArea(newArea) {
+      // 生成新区域的网格
+      const baseId = this.polygons.length + 1; // 使用当前多边形数量作为基础ID
+      const newAreaPolygons = this.generateSingleAreaGrid(newArea, baseId - 1);
+
+      // 创建并添加新的多边形
+      newAreaPolygons.forEach(polygonData => {
+        const colors = this.getPolygonColorByRiskLevel(polygonData.riskLevel || 'healthy');
+
+        const polygon = new AMap.Polygon({
+          path: [polygonData.path], // 多边形路径
+          fillColor: colors.fillColor, // 根据风险等级确定填充颜色
+          strokeOpacity: polygonData.strokeOpacity || 1, // 线条透明度
+          fillOpacity: polygonData.fillOpacity || 0.5, // 填充透明度
+          strokeColor: colors.strokeColor, // 根据风险等级确定边框颜色
+          strokeWeight: polygonData.strokeWeight || 1, // 线条宽度
+          strokeStyle: polygonData.strokeStyle || "solid", // 线样式
+          strokeDasharray: polygonData.strokeDasharray || [5, 5], // 虚线样式
+          extData: {
+            id: polygonData.id,
+            riskLevel: polygonData.riskLevel || 'healthy', // 风险等级
+            name: polygonData.name, // 区域名称
+            areaType: polygonData.areaType,
+            areaName: polygonData.areaName,
+            ...polygonData.extData
+          }
+        });
+
+        // 为每个多边形添加交互事件
+        this.addPolygonEvents(polygon);
+
+        // 添加到地图
+        this.map.add(polygon);
+
+        // 存储多边形实例以便后续操作
+        this.polygons.push(polygon);
+      });
+
+      console.log(`成功添加新区域: ${newArea.name}`);
+    },
+
+
+    /**
+    * 通过配置添加新区域的便捷方法
+    */
+    addAreaFromConfig(areaId, areaName, coordinates, cols = 18, rows = 7) {
+      const newArea = {
+        id: areaId,
+        name: areaName,
+        westLat: coordinates.westLat,
+        eastLat: coordinates.eastLat,
+        northLng: coordinates.northLng,
+        southLng: coordinates.southLng,
+        cols: cols,
+        rows: rows
+      };
+
+      this.addNewArea(newArea);
+    },
+    // 获取多边形颜色根据风险等级
+    getPolygonColorByRiskLevel(level) {
+      switch (level) {
+        case 'healthy': // 健康
+          return {
+            fillColor: '#90EE90', // 浅绿色
+            strokeColor: '#228B22', // 深绿色边框
+            hoverColor: '#32CD32' // 鼠标悬停时的颜色
+          };
+        case 'low_risk': // 轻度风险
+          return {
+            fillColor: '#FFFF99', // 浅黄色
+            strokeColor: '#FFA500', // 橙色边框
+            hoverColor: '#FFD700' // 鼠标悬停时的颜色
+          };
+        case 'high_risk': // 重度风险
+          return {
+            fillColor: '#FF6347', // 番茄红
+            strokeColor: '#DC143C', // 深红色边框
+            hoverColor: '#FF0000' // 鼠标悬停时的颜色
+          };
+        default:
+          return {
+            fillColor: '#ccebc5', // 默认颜色
+            strokeColor: '#2b8cbe',
+            hoverColor: '#7bccc4'
+          };
+      }
+    },
+
+    // 在 methods 中添加以下方法
+    // 修改 createMultiplePolygons 方法以兼容新的路径格式
+    createMultiplePolygons(polygonsData) {
+      this.polygons = []; // 存储所有多边形实例
+
+      polygonsData.forEach((polygonData, index) => {
+        const colors = this.getPolygonColorByRiskLevel(polygonData.riskLevel || 'healthy');
+
+        // 检查路径格式并相应处理
+        const path = Array.isArray(polygonData.path[0]) ? polygonData.path : [polygonData.path];
+
+        const polygon = new AMap.Polygon({
+          path: path, // 多边形路径
+          fillColor: colors.fillColor, // 根据风险等级确定填充颜色
+          strokeOpacity: polygonData.strokeOpacity || 1, // 线条透明度
+          fillOpacity: polygonData.fillOpacity || 0.5, // 填充透明度
+          strokeColor: colors.strokeColor, // 根据风险等级确定边框颜色
+          strokeWeight: polygonData.strokeWeight || 1, // 线条宽度
+          strokeStyle: polygonData.strokeStyle || "solid", // 线样式
+          strokeDasharray: polygonData.strokeDasharray || [5, 5], // 虚线样式
+          extData: {
+            id: polygonData.id || index, // 可以添加标识符
+            riskLevel: polygonData.riskLevel || 'healthy', // 风险等级
+            name: polygonData.name || `区域${index + 1}`, // 区域名称
+            areaType: polygonData.areaType || 'default',
+            ...polygonData.extData
+          }
+        });
+
+        // 为每个多边形添加交互事件
+        this.addPolygonEvents(polygon);
+
+        // 添加到地图
+        this.map.add(polygon);
+
+        // 存储多边形实例以便后续操作
+        this.polygons.push(polygon);
+      });
+    },
+
+    // 添加多边形交互事件
+    addPolygonEvents(polygon) {
+      // 鼠标移入更改样式
+      polygon.on("mouseover", () => {
+        const currentRiskLevel = polygon.getExtData().riskLevel;
+        const colors = this.getPolygonColorByRiskLevel(currentRiskLevel);
+
+        polygon.setOptions({
+          fillOpacity: 0.7, // 多边形填充透明度
+          fillColor: colors.hoverColor,
+        });
+      });
+
+      // 鼠标移出恢复样式
+      polygon.on("mouseout", () => {
+        const currentRiskLevel = polygon.getExtData().riskLevel;
+        const colors = this.getPolygonColorByRiskLevel(currentRiskLevel);
+
+        polygon.setOptions({
+          fillOpacity: 0.5,
+          fillColor: colors.fillColor,
+        });
+      });
+
+      // 鼠标点击事件
+      polygon.on("click", (e) => {
+        const extData = polygon.getExtData();
+        console.log(`点击了区域: ${extData.name}, 风险等级: ${extData.riskLevel}`);
+
+        // 创建或更新信息窗口
+        this.showInfoWindow(e.lnglat, extData);
+      });
+    },
+
+    // 显示高德地图信息窗口
+    showInfoWindow(position, extData) {
+      // 如果信息窗口不存在,则创建它
+      if (!this.infoWindow) {
+        this.infoWindow = new AMap.InfoWindow({
+          offset: new AMap.Pixel(0, -10),
+          closeWhenClickMap: true, // 点击地图关闭信息窗体
+          autoMove: true, // 自动调整位置,防止被地图遮挡
+          isCustom: false, // 使用默认样式
+          showShadow: true
+        });
+      }
+
+      // 构建信息窗口内容
+      const content = `
+        <div style="padding: 10px; min-width: 180px;">
+          <h4 style="margin: 0 0 8px 0; font-size: 16px; color: #333;">${extData.name}</h4>
+          <div style="display: flex; align-items: center; margin-bottom: 5px;">
+            <span style="display: inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${extData.riskLevel === 'healthy' ? '#228B22' :
+          extData.riskLevel === 'low_risk' ? '#FFA500' :
+            '#DC143C'
+        }; margin-right: 8px;"></span>
+            <strong>风险等级:</strong>
+            <span>${this.getRiskLevelText(extData.riskLevel)}</span>
+          </div>
+          <div style="font-size: 12px; color: #666; margin-top: 8px;">
+            点击坐标: ${position.lng.toFixed(6)}, ${position.lat.toFixed(6)}
+          </div>
+        </div>
+      `;
+
+      // 设置内容并打开信息窗口
+      this.infoWindow.setContent(content);
+      this.infoWindow.open(this.map, position);
+    },
+
+    // 获取风险等级文字描述
+    getRiskLevelText(riskLevel) {
+      switch (riskLevel) {
+        case 'healthy':
+          return '健康';
+        case 'low_risk':
+          return '轻度风险';
+        case 'high_risk':
+          return '重度风险';
+        default:
+          return '未知';
+      }
+    },
+    // 清理所有多边形
+    clearAllPolygons() {
+      if (this.polygons && this.polygons.length > 0) {
+        this.polygons.forEach(polygon => {
+          this.map.remove(polygon);
+        });
+        this.polygons = [];
+      }
+    },
+
+    /**
+  * 切换热力图显示/隐藏
+  */
+    toggleHeatmap() {
+      if (!this.heatmap) {
+        console.warn('热力图尚未初始化');
+        return;
+      }
+
+      if (this.heatmapVisible) {
+        // 隐藏热力图
+        this.heatmap.hide();
+        this.heatmapVisible = false;
+        console.log('热力图已隐藏');
+      } else {
+        // 显示热力图
+        this.heatmap.show();
+        this.heatmapVisible = true;
+        console.log('热力图已显示');
+      }
+    },
+
+
+    initMap() {
+      if (this.isMapLoaded) return;
+
+      try {
+        if (typeof AMap === 'undefined') {
+          console.error('高德地图API未加载,请检查script标签引入');
+          return;
+        }
+
+        // 初始化3D地图
+        this.map = new AMap.Map("container", {
+          center: [116.991074, 34.203813],
+          zoom: 18.5,
+          zooms: [3, 20],  // 设置缩放级别范围
+          pitch: 40.0,      // 俯仰角设置为42度
+          rotation: 4.9,    // 旋转角设置为4.9度
+          resizeEnable: true,
+          viewMode: '3D',
+          buildingAnimation: true,
+          skyColor: '#f0f9ff',
+          showLabel: true,
+          layers: [
+            new AMap.TileLayer.Satellite({ zIndex: 1, opacity: 1 }),
+            new AMap.TileLayer.RoadNet({ zIndex: 2, opacity: 0.9 })
+          ]
+        });
+
+        // 添加地图点击事件监听
+        this.map.on('click', (event) => {
+          const lnglat = event.lnglat;  // 获取点击的经纬度坐标
+          const lng = lnglat.lng;       // 经度
+          const lat = lnglat.lat;       // 纬度
+
+          console.log('地图点击坐标:', {
+            lng: lng,
+            lat: lat,
+            formatted: `${lng.toFixed(6)}, ${lat.toFixed(6)}`,
+            timestamp: new Date().toISOString()
+          });
+        });
+
+        // 初始化并添加边框
+        this.addBorderPolygon();
+
+        // 开始边框动画效果
+        this.startBorderAnimation();
+
+
+        // 准备多边形数据 - 根据风险等级着色
+        // 在 initMap 方法中替换原来的 polygonDataList
+        const polygonDataList = this.generateGridPolygons(); // 使用生成的网格数据
+
+        // 批量创建多边形
+        this.createMultiplePolygons(polygonDataList);
+
+        // 如果多边形设置为隐藏,则立即从地图上移除
+        if (!this.polygonVisible) {
+          this.polygons.forEach(polygon => {
+            this.map.remove(polygon);
+          });
+        }
+
+        // 加载热力图插件
+        AMap.plugin(['AMap.HeatMap'], () => {
+          console.log('热力图插件手动加载完成✅');
+
+          // 使用真实葡萄园数据
+          const heatmapData = [
+            { lng: 116.989156, lat: 34.203689, count: 82 },
+            { lng: 116.988789, lat: 34.204215, count: 47 },
+            { lng: 116.990023, lat: 34.202987, count: 91 },
+            { lng: 116.989678, lat: 34.203456, count: 29 },
+            { lng: 116.988654, lat: 34.203987, count: 76 },
+            { lng: 116.989876, lat: 34.204678, count: 63 },
+            { lng: 116.988345, lat: 34.202876, count: 58 },
+            { lng: 116.989456, lat: 34.203123, count: 88 },
+            { lng: 116.989987, lat: 34.204111, count: 37 },
+            { lng: 116.988987, lat: 34.202765, count: 95 },
+            { lng: 116.989234, lat: 34.204345, count: 42 },
+            { lng: 116.988567, lat: 34.203789, count: 71 },
+            { lng: 116.989765, lat: 34.203098, count: 66 },
+            { lng: 116.990123, lat: 34.203876, count: 28 },
+            { lng: 116.988432, lat: 34.204567, count: 89 },
+
+            { lng: 116.989098, lat: 34.202543, count: 85 },
+            { lng: 116.988678, lat: 34.203987, count: 41 },
+            { lng: 116.989432, lat: 34.204098, count: 97 },
+            { lng: 116.990345, lat: 34.202876, count: 25 },
+            { lng: 116.988543, lat: 34.203654, count: 74 },
+            { lng: 116.989789, lat: 34.204234, count: 56 },
+            { lng: 116.989123, lat: 34.202987, count: 81 },
+            { lng: 116.988901, lat: 34.204654, count: 39 },
+            { lng: 116.989876, lat: 34.203765, count: 69 },
+            { lng: 116.990098, lat: 34.204321, count: 90 },
+            { lng: 116.988456, lat: 34.203123, count: 45 },
+            { lng: 116.989567, lat: 34.202789, count: 77 },
+            { lng: 116.988890, lat: 34.204456, count: 59 },
+            { lng: 116.989345, lat: 34.203890, count: 87 },
+            { lng: 116.990198, lat: 34.204012, count: 31 },
+            { lng: 116.988790, lat: 34.203543, count: 62 }
+          ];
+
+          // 创建热力图实例
+          this.heatmap = new AMap.HeatMap(this.map, {
+            radius: 100,
+            opacity: [0.4, 0.8],
+            gradient: {
+              1: '#FF4C2F',
+              0.8: '#FAA53F',
+              0.6: '#FFF100',
+              0.5: '#7DF675',
+              0.4: '#5CE182',
+              0.2: '#29CF6F',
+            },
+          });
+
+          // 设置热力图数据
+          this.heatmap.setDataSet({
+            data: heatmapData,
+            max: 100
+          });
+
+          // 确保热力图初始状态为隐藏
+          this.heatmap.hide();
+
+          console.log('3D热力图初始化成功✅');
+        });
+
+        this.isMapLoaded = true;
+        console.log('3D地图初始化成功✅');
+
+      } catch (e) {
+        console.error("高德地图3D加载异常:", e);
+      }
+    },
+
+    /**
+     * 添加3D发光边框(使用Polygon)
+     */
+    addBorderPolygon() {
+      // 使用你提供的点击坐标点形成边界
+      const borderCoordinates = [
+        [116.988285, 34.204935],
+        [116.988215, 34.202335],
+        [116.990618, 34.202346],
+        [116.993520, 34.204017],
+        [116.993547, 34.204941]
+      ];
+
+      // 创建多边形边框
+      this.borderPolygon = new AMap.Polygon({
+        path: borderCoordinates, // 多边形轮廓的节点坐标数组
+        strokeColor: this.borderColor, // 线条颜色
+        strokeWeight: 3, // 线条宽度
+        strokeOpacity: 0.8, // 线条透明度
+        fillOpacity: 0, // 填充透明度,设为0表示不填充
+        strokeStyle: 'solid', // 线样式为实线
+        extData: {
+          type: 'border'
+        },
+        height: 100, // 设置多边形高度,实现墙体效果
+        zIndex: 10
+      });
+
+      // 将边框添加到地图上
+      this.map.add(this.borderPolygon);
+
+      console.log('3D发光边框初始化成功✅');
+    },
+
+    /**
+     * 开始边框动画效果(发光闪烁)
+     */
+    startBorderAnimation() {
+      // 清理之前的动画
+      this.stopBorderAnimation();
+
+      let opacity = 0.6;
+      let increasing = true;
+      let hue = 180; // 青色的色相值
+
+      this.animationInterval = setInterval(() => {
+        // 控制透明度变化,实现闪烁效果
+        if (increasing) {
+          opacity += 0.02;
+          if (opacity >= 1.0) {
+            increasing = false;
+          }
+        } else {
+          opacity -= 0.02;
+          if (opacity <= 0.4) {
+            increasing = true;
+          }
+        }
+
+        // 控制颜色变化,实现彩虹发光效果
+        hue = (hue + 0.5) % 360;
+        const animatedColor = this.hslToHex(hue, 100, 70); // HSL to Hex
+
+        if (this.borderPolygon) {
+          this.borderPolygon.setOptions({
+            strokeColor: animatedColor,
+            strokeOpacity: opacity
+          });
+        }
+      }, 50); // 每50毫秒更新一次动画
+    },
+
+    /**
+     * 停止边框动画效果
+     */
+    stopBorderAnimation() {
+      if (this.animationInterval) {
+        clearInterval(this.animationInterval);
+        this.animationInterval = null;
+      }
+      // 恢复原始颜色和透明度
+      if (this.borderPolygon) {
+        this.borderPolygon.setOptions({
+          strokeColor: this.borderColor,
+          strokeOpacity: 0.8
+        });
+      }
+    },
+
+    /**
+     * HSL颜色值转换为十六进制
+     * @param {number} h - 色相 (0-360)
+     * @param {number} s - 饱和度 (0-100)
+     * @param {number} l - 亮度 (0-100)
+     * @returns {string} 十六进制颜色值
+     */
+    hslToHex(h, s, l) {
+      h /= 360;
+      s /= 100;
+      l /= 100;
+
+      let r, g, b;
+
+      if (s === 0) {
+        r = g = b = l; // achromatic
+      } else {
+        const hue2rgb = (p, q, t) => {
+          if (t < 0) t += 1;
+          if (t > 1) t -= 1;
+          if (t < 1 / 6) return p + (q - p) * 6 * t;
+          if (t < 1 / 2) return q;
+          if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+          return p;
+        };
+
+        const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+        const p = 2 * l - q;
+
+        r = hue2rgb(p, q, h + 1 / 3);
+        g = hue2rgb(p, q, h);
+        b = hue2rgb(p, q, h - 1 / 3);
+      }
+
+      const toHex = x => {
+        const hex = Math.round(x * 255).toString(16);
+        return hex.length === 1 ? '0' + hex : hex;
+      };
+
+      return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+    },
+
+    /**
+     * 切换边框颜色(正常状态与告警状态)
+     * @param {boolean} isAlarm - 是否为告警状态
+     */
+    toggleBorderColor(isAlarm = false) {
+      this.isAlarm = isAlarm;
+      this.borderColor = isAlarm ? '#FF0000' : '#3dfcfc'; // 红色或青色
+
+      if (this.borderPolygon) {
+        this.borderPolygon.setOptions({
+          strokeColor: this.borderColor
+        });
+      }
+    },
+
+    /**
+     * 更新边框样式
+     * @param {object} options - 样式选项
+     */
+    updateBorderOptions(options) {
+      if (this.borderPolygon) {
+        this.borderPolygon.setOptions(options);
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+.amap-container {
+  width: 100%;
+  height: calc(100vh - 80px);
+}
+
+.control-panel {
+  position: absolute;
+  top: 20px;
+  left: 20px;
+  z-index: 1000;
+  display: flex;
+  gap: 10px;
+}
+
+.heatmap-toggle-btn {
+  padding: 8px 16px;
+  background-color: rgba(255, 255, 255, 0.9);
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+  transition: all 0.3s ease;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.heatmap-toggle-btn:hover {
+  background-color: #f5f5f5;
+  transform: translateY(-1px);
+}
+
+.heatmap-toggle-btn.active {
+  background-color: #409eff;
+  color: white;
+  border-color: #409eff;
+}
+
+.map-box {
+  width: 100%;
+  height: 100%;
+}
+
+#container {
+  width: 100%;
+  height: 100%;
+}
+
+.polygon-toggle-btn {
+  padding: 8px 16px;
+  background-color: rgba(255, 255, 255, 0.9);
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+  transition: all 0.3s ease;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.polygon-toggle-btn:hover {
+  background-color: #f5f5f5;
+  transform: translateY(-1px);
+}
+
+.polygon-toggle-btn.active {
+  background-color: #67c23a;
+  /* 绿色背景表示激活状态 */
+  color: white;
+  border-color: #67c23a;
+}
+</style>

+ 920 - 0
src/views/base/batch/index.vue

@@ -0,0 +1,920 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="批次号" prop="batchNo">
+        <el-input
+          v-model="queryParams.batchNo"
+          placeholder="请输入批次号"
+          clearable
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="商品名称" prop="productName">
+        <el-input
+          v-model="queryParams.productName"
+          placeholder="请输入商品名称"
+          clearable
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <!-- <el-form-item label="商品规格" prop="productSpec">
+        <el-input
+          v-model="queryParams.productSpec"
+          placeholder="请输入商品规格"
+          clearable
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item> -->
+      <el-form-item label="农场名称" prop="farmName">
+        <el-input
+          v-model="queryParams.farmName"
+          placeholder="请输入农场名称"
+          clearable
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="农场所在地" prop="farmRegion">
+        <el-input
+          v-model="queryParams.farmRegion"
+          placeholder="请输入农场所在地"
+          clearable
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <!-- <el-form-item label="生产/采收日期" prop="produceDate">
+        <el-date-picker clearable
+          v-model="queryParams.produceDate"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="请选择生产/采收日期">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="包装日期" prop="packageDate">
+        <el-date-picker clearable
+          v-model="queryParams.packageDate"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="请选择包装日期">
+        </el-date-picker>
+      </el-form-item> -->
+       <el-form-item label="生产/采收日期" prop="produceDate">
+        <el-date-picker clearable
+          v-model="dateRange.produceDate"
+          type="daterange"
+          value-format="yyyy-MM-dd"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="包装日期" prop="packageDate">
+        <el-date-picker clearable
+          v-model="dateRange.packageDate"
+          type="daterange"
+          value-format="yyyy-MM-dd"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
+          <el-option
+            v-for="dict in dict.type.batch_status"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <!-- <el-form-item label="创建时间" prop="createdAt">
+        <el-date-picker clearable
+          v-model="queryParams.createdAt"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="请选择创建时间">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="更新时间" prop="updatedAt">
+        <el-date-picker clearable
+          v-model="queryParams.updatedAt"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="请选择更新时间">
+        </el-date-picker>
+      </el-form-item> -->
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['base:batch:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="single"
+          @click="handleUpdate"
+          v-hasPermi="['base:batch:edit']"
+        >修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['base:batch:remove']"
+        >删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          @click="handleExport"
+          v-hasPermi="['base:batch:export']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="batchList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <!-- <el-table-column label="主键ID" align="center" prop="id" /> -->
+      <el-table-column label="批次号" align="center" prop="batchNo" />
+      <el-table-column label="商品名称" align="center" prop="productName" />
+      <el-table-column label="商品规格" align="center" prop="productSpec" />
+      <!-- <el-table-column label="商品图片URL" align="center" prop="productImage" width="100">
+        <template slot-scope="scope">
+          <image-preview :src="scope.row.productImage" :width="50" :height="50"/>
+        </template>
+      </el-table-column> -->
+      <el-table-column label="商品简介" align="center" prop="productDesc" />
+      <el-table-column label="农场名称" align="center" prop="farmName" />
+      <el-table-column label="农场所在地" align="center" prop="farmRegion" />
+      <!-- <el-table-column label="农场图片URL" align="center" prop="farmImage" width="100">
+        <template slot-scope="scope">
+          <image-preview :src="scope.row.farmImage" :width="50" :height="50"/>
+        </template>
+      </el-table-column> -->
+      <el-table-column label="农场简介" align="center" prop="farmIntro" />
+      <el-table-column label="生产/采收日期" align="center" prop="produceDate" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.produceDate, '{y}-{m}-{d}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="包装日期" align="center" prop="packageDate" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.packageDate, '{y}-{m}-{d}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <dict-tag :options="dict.type.batch_status" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <!-- <el-table-column label="创建时间" align="center" prop="createdAt" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createdAt, '{y}-{m}-{d}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="更新时间" align="center" prop="updatedAt" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.updatedAt, '{y}-{m}-{d}') }}</span>
+        </template>
+      </el-table-column> -->
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['base:batch:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['base:batch:remove']"
+          >删除</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-share"
+            @click="handleGenerateQrCode(scope.row)"
+          >生成二维码</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-document"
+            @click="handleUploadCertificate(scope.row)"
+          >上传合格证</el-button>
+
+        </template>
+      </el-table-column>
+    </el-table>
+    
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改批次管理对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="批次号" prop="batchNo">
+          <el-input v-model="form.batchNo" placeholder="请输入批次号" />
+        </el-form-item>
+        <el-form-item label="商品名称" prop="productName">
+          <el-input v-model="form.productName" placeholder="请输入商品名称" />
+        </el-form-item>
+        <el-form-item label="商品规格" prop="productSpec">
+          <el-input v-model="form.productSpec" placeholder="请输入商品规格" />
+        </el-form-item>
+        <el-form-item label="商品图片" prop="productImage">
+          <image-upload v-model="form.productImage"/>
+        </el-form-item>
+        <el-form-item label="商品简介" prop="productDesc">
+          <el-input v-model="form.productDesc" type="textarea" placeholder="请输入内容" />
+        </el-form-item>
+        <el-form-item label="农场名称" prop="farmName">
+          <el-input v-model="form.farmName" placeholder="请输入农场名称" />
+        </el-form-item>
+        <el-form-item label="农场所在地" prop="farmRegion">
+          <el-input v-model="form.farmRegion" placeholder="请输入农场所在地" />
+        </el-form-item>
+        <el-form-item label="农场图片" prop="farmImage">
+          <image-upload v-model="form.farmImage"/>
+        </el-form-item>
+        <el-form-item label="农场简介" prop="farmIntro">
+          <el-input v-model="form.farmIntro" type="textarea" placeholder="请输入内容" />
+        </el-form-item>
+        <el-form-item label="生产/采收日期" prop="produceDate">
+          <el-date-picker clearable
+            v-model="form.produceDate"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="请选择生产/采收日期">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="包装日期" prop="packageDate">
+          <el-date-picker clearable
+            v-model="form.packageDate"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="请选择包装日期">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in dict.type.batch_status"
+              :key="dict.value"
+              :label="dict.value"
+            >{{dict.label}}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <!-- <el-form-item label="创建时间" prop="createdAt">
+          <el-date-picker clearable
+            v-model="form.createdAt"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="请选择创建时间">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="更新时间" prop="updatedAt">
+          <el-date-picker clearable
+            v-model="form.updatedAt"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="请选择更新时间">
+          </el-date-picker>
+        </el-form-item> -->
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+     <!-- 生成二维码弹窗 -->
+    <el-dialog :title="qrTitle" :visible.sync="qrVisible" width="420px" append-to-body>
+      <div class="qr-card" ref="qrCard">
+        <div class="qr-header">
+          <div class="brand-name">{{ qrData.brandName }}</div>
+          <div class="brand-en">{{ qrData.brandEn }}</div>
+        </div>
+        <div class="qr-content">
+          <div class="scan-tip">扫码查看溯源信息</div>
+          <div class="batch-info">批次号:{{ qrData.batchNo }}</div>
+          <div class="origin">{{ qrData.origin }}</div>
+        </div>
+        <div class="qr-code-wrapper">
+          <canvas id="qrCanvas"></canvas>
+        </div>
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="handleDownloadQrCode">下载二维码</el-button>
+        <el-button @click="qrVisible = false">关闭</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 上传合格证弹窗 -->
+    <el-dialog :title="certTitle" :visible.sync="certVisible" width="600px" append-to-body>
+      <el-form ref="certForm" :model="certForm" :rules="certRules" label-width="100px">
+        <el-form-item label="批次号">
+          <span>{{ certForm.batchNo }}</span>
+        </el-form-item>
+        <el-form-item label="合格证编号" prop="certNo">
+          <el-input v-model="certForm.certNo" placeholder="请输入合格证编号" />
+        </el-form-item>
+        <el-form-item label="开具日期" prop="certIssueDate">
+          <el-date-picker
+            v-model="certForm.certIssueDate"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="请选择开具日期"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="状态" prop="certStatus">
+          <el-select v-model="certForm.certStatus" placeholder="请选择状态">
+            <el-option label="已上传" value="uploaded" />
+            <el-option label="待上传" value="pending" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="合格证文件" prop="certFiles">
+          <file-upload v-model="certForm.certFiles" :limit="5" />
+          <div class="el-upload__tip">
+            支持上传多个文件,总数量不超过 5 个
+          </div>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitCertForm">确 定</el-button>
+        <el-button @click="cancelCert">取 消</el-button>
+      </div>
+    </el-dialog>
+
+  
+  </div>
+</template>
+
+<script>
+import { listBatch, getBatch, delBatch, addBatch, updateBatch } from "@/api/base/batch"
+import QRCode from 'qrcode'
+import { listReport, getReport, addReport, updateReport } from "@/api/base/report"
+import { listCertificate, getCertificate, addCertificate, updateCertificate } from "@/api/base/certificate"
+
+
+
+export default {
+  name: "Batch",
+  dicts: ['batch_status'],
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 批次管理表格数据
+      batchList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+
+      // 日期范围
+      dateRange: {
+        produceDate: [],
+        packageDate: []
+      },
+
+      // 是否显示二维码弹窗
+      qrVisible: false,
+      // 二维码弹窗标题
+      qrTitle: "生成二维码",
+      // 二维码数据
+      qrData: {
+        brandName: "爱智农",
+        brandEn: "AIZHONGNONG",
+        batchNo: "",
+        origin: "",
+        qrUrl: ""
+      },
+      // 是否显示合格证弹窗
+      certVisible: false,
+      // 合格证弹窗标题
+      certTitle: "",
+      // 合格证表单
+      certForm: {
+        id: null,
+        batchId: null,
+        batchNo: "",
+        certNo: "",
+        certIssueDate: null,
+        certStatus: "pending",
+        certFiles: []
+      },
+      // 合格证表单校验规则
+      certRules: {
+        certNo: [
+          { required: true, message: "合格证编号不能为空", trigger: "blur" }
+        ],
+        certIssueDate: [
+          { required: true, message: "开具日期不能为空", trigger: "change" }
+        ],
+        certStatus: [
+          { required: true, message: "状态不能为空", trigger: "change" }
+        ]
+      },
+     
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        batchNo: null,
+        productName: null,
+        productSpec: null,
+        productImage: null,
+        productDesc: null,
+        farmName: null,
+        farmRegion: null,
+        farmImage: null,
+        farmIntro: null,
+        produceDate: null,
+        packageDate: null,
+        produceDateStart: null,
+        produceDateEnd: null,
+        packageDateStart: null,
+        packageDateEnd: null,
+        status: null,
+        createdAt: null,
+        updatedAt: null
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        batchNo: [
+          { required: true, message: "批次号不能为空", trigger: "blur" }
+        ],
+        productName: [
+          { required: true, message: "商品名称不能为空", trigger: "blur" }
+        ],
+        farmName: [
+          { required: true, message: "农场名称不能为空", trigger: "blur" }
+        ],
+        farmRegion: [
+          { required: true, message: "农场所在地不能为空", trigger: "blur" }
+        ],
+        status: [
+          { required: true, message: "状态不能为空", trigger: "change" }
+        ],
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    /** 查询批次管理列表 */
+    getList() {
+      this.loading = true
+      listBatch(this.queryParams).then(response => {
+        this.batchList = response.rows
+        this.total = response.total
+        this.loading = false
+      })
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false
+      this.reset()
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        batchNo: null,
+        productName: null,
+        productSpec: null,
+        productImage: null,
+        productDesc: null,
+        farmName: null,
+        farmRegion: null,
+        farmImage: null,
+        farmIntro: null,
+        produceDate: null,
+        packageDate: null,
+        status: null,
+        createdAt: null,
+        updatedAt: null
+      }
+      this.resetForm("form")
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.queryParams.produceDateStart = this.dateRange.produceDate?.[0] || null
+      this.queryParams.produceDateEnd = this.dateRange.produceDate?.[1] || null
+      this.queryParams.packageDateStart = this.dateRange.packageDate?.[0] || null
+      this.queryParams.packageDateEnd = this.dateRange.packageDate?.[1] || null
+      this.getList()
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRange.produceDate = []
+      this.dateRange.packageDate = []
+      this.resetForm("queryForm")
+      this.handleQuery()
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset()
+      this.open = true
+      this.title = "添加批次管理"
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset()
+      const id = row.id || this.ids
+      getBatch(id).then(response => {
+        this.form = response.data
+        this.open = true
+        this.title = "修改批次管理"
+      })
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateBatch(this.form).then(response => {
+              this.$modal.msgSuccess("修改成功")
+              this.open = false
+              this.getList()
+            })
+          } else {
+            addBatch(this.form).then(response => {
+              this.$modal.msgSuccess("新增成功")
+              this.open = false
+              this.getList()
+            })
+          }
+        }
+      })
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids
+      this.$modal.confirm('是否确认删除批次管理编号为"' + ids + '"的数据项?').then(function() {
+        return delBatch(ids)
+      }).then(() => {
+        this.getList()
+        this.$modal.msgSuccess("删除成功")
+      }).catch(() => {})
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      this.download('base/batch/export', {
+        ...this.queryParams
+      }, `batch_${new Date().getTime()}.xlsx`)
+    },
+    /** 生成二维码 */
+    async handleGenerateQrCode(row) {
+      this.qrData.batchNo = row.batchNo
+      this.qrData.farmRegion = row.farmRegion || '未知产地'
+      // 这里替换成你的溯源页面地址,可以根据实际域名配置
+     // const baseUrl = window.location.origin
+      const baseUrl = 'https://nxy.gbdfarm.com:9001'
+      this.qrData.qrUrl = `${baseUrl}/${row.id}`
+      
+      this.qrVisible = true
+      
+      // 等待 DOM 渲染后生成二维码
+      this.$nextTick(async () => {
+        const canvas = document.getElementById('qrCanvas')
+        if (canvas) {
+          try {
+            await QRCode.toCanvas(canvas, this.qrData.qrUrl, {
+              width: 150,
+              height: 150,
+              margin: 2,
+              color: {
+                dark: '#333333',
+                light: '#ffffff'
+              }
+            })
+          } catch (error) {
+            console.error('二维码生成失败:', error)
+            this.$message.error('二维码生成失败,请重试')
+          }
+        }
+      })
+    },
+
+
+     /** 下载二维码卡片 */
+    async handleDownloadQrCode() {
+      const cardElement = this.$refs.qrCard
+      if (!cardElement) {
+        this.$message.warning('卡片未加载,请重试')
+        return
+      }
+
+      try {
+        // 动态导入 html2canvas
+        const html2canvas = (await import('html2canvas')).default
+        
+        // 将整个卡片转换为图片
+        const canvas = await html2canvas(cardElement, {
+          backgroundColor: '#ffffff',
+          scale: 2, // 提高清晰度
+          useCORS: true,
+          logging: false
+        })
+        
+        // 转为图片并下载
+        const imgUrl = canvas.toDataURL('image/png')
+        const link = document.createElement('a')
+        link.download = `溯源卡片_${this.qrData.batchNo}.png`
+        link.href = imgUrl
+        link.click()
+        
+        this.$message.success('下载成功')
+      } catch (error) {
+        console.error('下载失败:', error)
+        this.$message.error('下载失败,请重试')
+      }
+    },
+     /** 上传合格证按钮操作 */
+    handleUploadCertificate(row) {
+      this.resetCert()
+      this.certForm.batchId = row.id
+      this.certForm.batchNo = row.batchNo
+      this.certVisible = true
+      this.certTitle = "上传合格证"
+      
+      // 查询该批次是否已有合格证
+      listCertificate({ batchId: row.id }).then(response => {
+        if (response.rows && response.rows.length > 0) {
+          // 已有合格证,设置为编辑模式
+          const cert = response.rows[0]
+          this.certForm = {
+            id: cert.id,
+            batchId: cert.batchId,
+            batchNo: row.batchNo,
+            certNo: cert.certNo,
+            certIssueDate: cert.certIssueDate,
+            certStatus: cert.certStatus,
+            certFiles: []
+          }
+          
+          // 解析 certFiles 字段(JSON 数组)
+          if (cert.certFiles) {
+            try {
+              const parsedFiles = JSON.parse(cert.certFiles)
+              if (Array.isArray(parsedFiles)) {
+                // 确保每个文件对象都有 name 属性
+                this.certForm.certFiles = parsedFiles.map(file => {
+                  if (!file.name && file.url) {
+                    const lastSlashIndex = file.url.lastIndexOf('/')
+                    file.name = lastSlashIndex > -1 ? file.url.substring(lastSlashIndex + 1) : file.url
+                  }
+                  return file
+                })
+              } else {
+                if (!parsedFiles.name && parsedFiles.url) {
+                  const lastSlashIndex = parsedFiles.url.lastIndexOf('/')
+                  parsedFiles.name = lastSlashIndex > -1 ? parsedFiles.url.substring(lastSlashIndex + 1) : parsedFiles.url
+                }
+                this.certForm.certFiles = [parsedFiles]
+              }
+            } catch (e) {
+              console.error('certFiles JSON 解析失败:', e)
+              this.certForm.certFiles = []
+            }
+          }
+          
+          this.certTitle = "编辑合格证"
+        }
+      })
+    },
+    /** 取消合格证 */
+    cancelCert() {
+      this.certVisible = false
+      this.resetCert()
+    },
+    /** 重置合格证表单 */
+    resetCert() {
+      this.certForm = {
+        id: null,
+        batchId: null,
+        batchNo: "",
+        certNo: "",
+        certIssueDate: null,
+        certStatus: "pending",
+        certFiles: []
+      }
+      this.resetForm("certForm")
+    },
+    /** 提交合格证表单 */
+    /** 提交合格证表单 */
+    submitCertForm() {
+      this.$refs["certForm"].validate(valid => {
+        if (valid) {
+          // 确保 certFiles 是正确的格式
+          let certFilesArray = []
+          
+          if (Array.isArray(this.certForm.certFiles)) {
+            // 如果已经是数组,确保每个文件都有 name 属性
+            certFilesArray = this.certForm.certFiles.map(file => {
+              if (!file.name && file.url) {
+                const lastSlashIndex = file.url.lastIndexOf('/')
+                file.name = lastSlashIndex > -1 ? file.url.substring(lastSlashIndex + 1) : file.url
+              }
+              return file
+            })
+          } else if (typeof this.certForm.certFiles === 'string') {
+            // 如果是字符串,先尝试解析为 JSON
+            try {
+              const parsed = JSON.parse(this.certForm.certFiles)
+              if (Array.isArray(parsed)) {
+                certFilesArray = parsed.map(file => {
+                  if (!file.name && file.url) {
+                    const lastSlashIndex = file.url.lastIndexOf('/')
+                    file.name = lastSlashIndex > -1 ? file.url.substring(lastSlashIndex + 1) : file.url
+                  }
+                  return file
+                })
+              } else {
+                certFilesArray = [{ 
+                  url: typeof parsed === 'string' ? parsed : parsed.url,
+                  name: typeof parsed === 'string' ? parsed.split('/').pop() : parsed.url?.split('/').pop()
+                }]
+              }
+            } catch (e) {
+              // JSON 解析失败,说明是逗号分隔的字符串
+              console.warn('certFiles 不是 JSON 格式,按逗号分隔处理:', e)
+              if (this.certForm.certFiles.trim()) {
+                certFilesArray = this.certForm.certFiles.split(',').map(url => {
+                  const trimmedUrl = url.trim()
+                  return {
+                    url: trimmedUrl,
+                    name: trimmedUrl.split('/').pop() || trimmedUrl
+                  }
+                })
+              }
+            }
+          }
+          
+          // 准备提交数据(使用大驼峰命名)
+          const data = {
+            batchId: this.certForm.batchId,
+            certNo: this.certForm.certNo,
+            certIssueDate: this.certForm.certIssueDate,
+            certStatus: this.certForm.certStatus,
+            // 关键:转为 JSON 字符串存储到数据库
+            certFiles: JSON.stringify(certFilesArray)
+          }
+          
+          if (this.certForm.id != null) {
+            // 修改
+            updateCertificate({ ...data, id: this.certForm.id }).then(response => {
+              this.$modal.msgSuccess("修改成功")
+              this.certVisible = false
+              this.resetCert()
+            })
+          } else {
+            // 新增
+            addCertificate(data).then(response => {
+              this.$modal.msgSuccess("上传成功")
+              this.certVisible = false
+              this.resetCert()
+            })
+          }
+        }
+      })
+    },
+  }
+}
+</script>
+<style scoped>
+.qr-card {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30px 20px;
+  border: 2px solid #e0e0e0;
+  border-radius: 12px;
+  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+  font-size: 14px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.qr-header {
+  flex: 1;
+  text-align: left;
+  padding-right: 15px;
+}
+
+.brand-name {
+  font-size: 22px;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 8px;
+  letter-spacing: 1px;
+}
+
+.brand-en {
+  font-size: 12px;
+  color: #666;
+  margin-bottom: 15px;
+  letter-spacing: 0.5px;
+}
+
+.qr-content {
+  flex: 1;
+  text-align: left;
+  padding: 0 15px;
+}
+
+.scan-tip {
+  margin-bottom: 12px;
+  color: #333;
+  font-weight: 500;
+  font-size: 13px;
+}
+
+.batch-info {
+  margin-bottom: 10px;
+  font-weight: 600;
+  color: #2c3e50;
+  font-size: 15px;
+}
+
+.origin {
+  color: #666;
+  font-size: 13px;
+}
+
+.qr-code-wrapper {
+  width: 160px;
+  height: 160px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #fff;
+  border: 1px solid #ddd;
+  border-radius: 8px;
+  padding: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+#qrCanvas {
+  width: 150px;
+  height: 150px;
+}
+
+.dialog-footer {
+  text-align: center;
+  padding-top: 10px;
+}
+</style>

文件差異過大導致無法顯示
+ 364 - 249
src/views/base/device/device-monitor.vue


+ 174 - 57
src/views/base/device/index.vue

@@ -230,7 +230,8 @@
                 <div class="status-item">
                   <div class="status-label">电量状态</div>
                   <div class="status-value">
-                    <el-progress :percentage="deviceStatus.battery" :stroke-width="8" class="battery-progress"></el-progress>
+                    <el-progress :percentage="deviceStatus.battery" :stroke-width="8"
+                      class="battery-progress"></el-progress>
                     <span class="battery-text">{{ deviceStatus.battery }}%</span>
                   </div>
                 </div>
@@ -415,7 +416,8 @@
                 <div class="status-item">
                   <div class="status-label">电量状态</div>
                   <div class="status-value">
-                    <el-progress :percentage="deviceStatus.battery" :stroke-width="8" class="battery-progress"></el-progress>
+                    <el-progress :percentage="deviceStatus.battery" :stroke-width="8"
+                      class="battery-progress"></el-progress>
                     <span class="battery-text">{{ deviceStatus.battery }}%</span>
                   </div>
                 </div>
@@ -428,10 +430,13 @@
           </div>
         </el-tab-pane>
         <el-tab-pane label="实时数据" name="realTimeData">
-
-          <iframe :src="iframeSrc" style="width: 100%; height: 350px;"></iframe>
-
-
+          <div v-if="formView.status == 0" class="device-offline-tip">
+            <el-empty description="设备离线,无法查看实时视频" :image-size="200">
+              <el-button type="primary" @click="handleQuery">刷新状态</el-button>
+            </el-empty>
+          </div>
+          <!-- ref 改为 videoPlayer,更语义化 -->
+          <video v-else ref="videoPlayer" controls autoplay style="width: 100%; height: 350px;"></video>
         </el-tab-pane>
       </el-tabs>
     </el-dialog>
@@ -518,7 +523,8 @@
                 <div class="status-item">
                   <div class="status-label">电量状态</div>
                   <div class="status-value">
-                    <el-progress :percentage="deviceStatus.battery" :stroke-width="8" class="battery-progress"></el-progress>
+                    <el-progress :percentage="deviceStatus.battery" :stroke-width="8"
+                      class="battery-progress"></el-progress>
                     <span class="battery-text">{{ deviceStatus.battery }}%</span>
                   </div>
                 </div>
@@ -749,7 +755,8 @@
                 <div class="status-item">
                   <div class="status-label">电量状态</div>
                   <div class="status-value">
-                    <el-progress :percentage="deviceStatus.battery" :stroke-width="8" class="battery-progress"></el-progress>
+                    <el-progress :percentage="deviceStatus.battery" :stroke-width="8"
+                      class="battery-progress"></el-progress>
                     <span class="battery-text">{{ deviceStatus.battery }}%</span>
                   </div>
                 </div>
@@ -823,13 +830,14 @@ import { deptTreeSelect } from "@/api/system/user";
 import Treeselect from "@riophae/vue-treeselect";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
 import * as  echarts from 'echarts'
-
+import flvjs from 'flv.js'  // 添加这行
 export default {
   name: "Device",
   components: { Treeselect },
   dicts: ['device_type', 'device_status'],
   data() {
     return {
+      flvPlayer: null,
       activeTab: 'basicInfo',
       activeWeatherTab: 'basicInfoWeather',
       activeControllerTab: 'basicInfoController',
@@ -1052,14 +1060,52 @@ export default {
   },
   methods: {
 
+    // 初始化 FLV 播放器
+    initFlvPlayer(videoUrl) {
+      // 先销毁旧播放器
+      if (this.flvPlayer) {
+        this.flvPlayer.destroy();
+        this.flvPlayer = null;
+      }
+
+      // 检查浏览器是否支持
+      if (flvjs.isSupported()) {
+        this.flvPlayer = flvjs.createPlayer({
+          type: 'flv',
+          url: videoUrl,
+          isLive: true,  // 直播流
+          cors: true     // 允许跨域
+        });
+
+        const videoElement = this.$refs.videoPlayer;
+        if (videoElement) {
+          this.flvPlayer.attachMediaElement(videoElement);
+          this.flvPlayer.load();
+          this.flvPlayer.play().catch(err => {
+            console.error('播放失败:', err);
+            this.$message.error('视频播放失败,请检查设备状态');
+          });
+        }
+      } else {
+        this.$message.error('浏览器不支持 FLV 播放');
+      }
+    },
+
     // 关闭摄像头对话框执行的操作
     handleDialogClose() {
       const query = { deviceId: this.deviceId };
-
       stopPlaying(query).then(response => {
         console.log(response)
-
       })
+
+      // 销毁播放器
+      if (this.flvPlayer) {
+        this.flvPlayer.destroy();
+        this.flvPlayer = null;
+      }
+
+      this.iframeSrc = '';
+      this.activeTab = 'basicInfo';
     },
 
 
@@ -1547,39 +1593,49 @@ export default {
       })
     },
     /** 查看按钮操作 */
+
     handleView(row) {
       this.resetView()
       const id = row.id || this.ids
       this.deviceId = row.deviceId
+
+      // 重置所有 tab 状态
+      this.activeTab = 'basicInfo';
+      this.activeWeatherTab = 'basicInfoWeather';
+      this.activeControllerTab = 'basicInfoController';
+      this.iframeSrc = '';
+
       getDevice(id).then(response => {
         this.formView = response.data
         this.formView.deptName = response.data.dept.deptName
         this.title = "设备详情"
+
+        // 只有在线状态才请求视频流
+        if (row.deviceTypeId == 2 && row.status != 0) {
+          const query = { deviceId: this.deviceId };
+          playback(query).then(res => {
+            // 使用写死的测试地址
+            const videoUrl = "wss://nxy.gbdfarm.com:9000/rtp/34020000001110000001_34020000001320000012.live.flv";
+
+            // 等待对话框打开后再初始化播放器
+            this.$nextTick(() => {
+              this.initFlvPlayer(videoUrl);
+            });
+          }).catch(error => {
+            this.$message.error("摄像头连接失败")
+          });
+        }
       })
-      if (row.deviceTypeId == 1) {// 传感器
 
+      if (row.deviceTypeId == 1) {
         this.openView = true
-
-      } else if (row.deviceTypeId == 2) {// 摄像头
-
-        const query = { deviceId: this.deviceId };
-
-        playback(query).then(response => {
-          console.log(response)
-          this.iframeSrc = "http://121.4.16.100:28080/#/play/wasm/" + encodeURIComponent(response.msg);
-          
-        }).catch(error => {
-          this.$message.error("摄像头连接失败")
-        });
+      } else if (row.deviceTypeId == 2) {
         this.cameraView = true
-
-
-      } else if (row.deviceTypeId == 3) {// 控制器
+      } else if (row.deviceTypeId == 3) {
         this.openControllerView = true;
-      } else if (row.deviceTypeId == 4) {// 气象设备
+      } else if (row.deviceTypeId == 4) {
         this.openWeatherView = true
       }
-
     },
     /** 提交按钮 */
     submitForm() {
@@ -1623,6 +1679,16 @@ export default {
 
 
 <style scoped>
+/* 设备离线提示样式 */
+.device-offline-tip {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 350px;
+  background: #f5f7fa;
+  border-radius: 8px;
+}
+
 li {
   list-style-type: none;
 }
@@ -1709,16 +1775,45 @@ ul {
 }
 
 /* 不同数据类型的颜色 */
-.weather-item-value.temperature { color: #059669; }
-.weather-item-value.humidity { color: #2563eb; }
-.weather-item-value.rainfall { color: #0891b2; }
-.weather-item-value.wind-direction { color: #7c3aed; }
-.weather-item-value.wind-speed { color: #059669; }
-.weather-item-value.pressure { color: #2563eb; }
-.weather-item-value.light { color: #f59e0b; }
-.weather-item-value.soil-n { color: #dc2626; }
-.weather-item-value.soil-p { color: #7c2d12; }
-.weather-item-value.soil-k { color: #365314; }
+.weather-item-value.temperature {
+  color: #059669;
+}
+
+.weather-item-value.humidity {
+  color: #2563eb;
+}
+
+.weather-item-value.rainfall {
+  color: #0891b2;
+}
+
+.weather-item-value.wind-direction {
+  color: #7c3aed;
+}
+
+.weather-item-value.wind-speed {
+  color: #059669;
+}
+
+.weather-item-value.pressure {
+  color: #2563eb;
+}
+
+.weather-item-value.light {
+  color: #f59e0b;
+}
+
+.weather-item-value.soil-n {
+  color: #dc2626;
+}
+
+.weather-item-value.soil-p {
+  color: #7c2d12;
+}
+
+.weather-item-value.soil-k {
+  color: #365314;
+}
 
 /* 统计数据卡片样式 */
 .weather-stats-card {
@@ -1773,20 +1868,40 @@ ul {
 }
 
 /* 统计数据颜色 */
-.weather-stats-value.temperature { color: #059669; }
-.weather-stats-value.humidity { color: #2563eb; }
-.weather-stats-value.rainfall { color: #0891b2; }
-.weather-stats-value.wind-speed { color: #7c3aed; }
-.weather-stats-value.soil-n { color: #dc2626; }
-.weather-stats-value.soil-p { color: #7c2d12; }
-.weather-stats-value.soil-k { color: #365314; }
+.weather-stats-value.temperature {
+  color: #059669;
+}
+
+.weather-stats-value.humidity {
+  color: #2563eb;
+}
+
+.weather-stats-value.rainfall {
+  color: #0891b2;
+}
+
+.weather-stats-value.wind-speed {
+  color: #7c3aed;
+}
+
+.weather-stats-value.soil-n {
+  color: #dc2626;
+}
+
+.weather-stats-value.soil-p {
+  color: #7c2d12;
+}
+
+.weather-stats-value.soil-k {
+  color: #365314;
+}
 
 /* 响应式设计 */
 @media (max-width: 1200px) {
   .weather-grid {
     grid-template-columns: repeat(4, 1fr);
   }
-  
+
   .weather-stats-grid {
     grid-template-columns: repeat(3, 1fr);
   }
@@ -1797,23 +1912,23 @@ ul {
     grid-template-columns: repeat(2, 1fr);
     gap: 12px;
   }
-  
+
   .weather-stats-grid {
     grid-template-columns: repeat(2, 1fr);
     gap: 12px;
   }
-  
+
   .weather-item,
   .weather-stats-item {
     padding: 16px 12px;
     height: 90px;
   }
-  
+
   .weather-item-value,
   .weather-stats-value {
     font-size: 18px;
   }
-  
+
   .weather-item-title,
   .weather-stats-title {
     font-size: 13px;
@@ -1821,6 +1936,7 @@ ul {
 }
 
 @media (max-width: 480px) {
+
   .weather-grid,
   .weather-stats-grid {
     grid-template-columns: 1fr;
@@ -1926,28 +2042,29 @@ ul {
 
 /* 响应式设计 */
 @media (max-width: 768px) {
+
   .info-grid,
   .status-grid {
     grid-template-columns: 1fr;
     gap: 16px;
     padding: 16px 0;
   }
-  
+
   .info-item,
   .status-item {
     gap: 6px;
   }
-  
+
   .info-label,
   .status-label {
     font-size: 13px;
   }
-  
+
   .info-value,
   .status-value {
     font-size: 14px;
   }
-  
+
   .battery-progress {
     max-width: 100px;
   }
@@ -1957,11 +2074,11 @@ ul {
   .device-basic-info {
     padding: 0 10px;
   }
-  
+
   .info-card {
     margin-bottom: 16px;
   }
-  
+
   .card-title {
     font-size: 15px;
   }

+ 62 - 84
src/views/base/machineWorkRecords/index.vue

@@ -148,7 +148,7 @@
       @pagination="getList" />
 
     <!-- 添加或修改农机作业记录对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
+    <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body @opened="queryMachineNameFieldUserTe()">
       <el-form ref="form" :model="form" :rules="rules" label-width="100px">
         <!-- 编辑时显示作业单号 -->
         <el-row :gutter="20" v-if="title.indexOf('修改') !== -1">
@@ -162,11 +162,7 @@
         <!-- 第一步:选择所属农场(必填) -->
         <el-row :gutter="20">
           <el-col :span="24">
-            <!-- <el-form-item label="所属农场" prop="farmName">
-              <el-select v-model="form.farmName" placeholder="请先选择所属农场" style="width: 100%" @change="handleFarmChange">
-                <el-option v-for="farm in farmOptions" :key="farm.value" :label="farm.label" :value="farm.value" />
-              </el-select>
-            </el-form-item> -->
+
             <el-form-item label="所属农场" prop="farmId">
               <treeselect v-model="form.farmId" :options="enabledDeptOptions" :show-count="true"
                 @input="queryMachineNameFieldUser(form.farmId)" placeholder="请先选择所属农场" />
@@ -178,12 +174,6 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="农机名称" prop="machineId">
-              <!-- <el-select v-model="form.machineName" placeholder="请选择农机" style="width: 100%"
-                @change="handleMachineNameChange" :disabled="!form.farmName">
-                <el-option v-for="machine in filteredMachineOptions" :key="machine.value" :label="machine.label"
-                  :value="machine.value" />
-              </el-select> -->
-
               <el-select v-model="form.machineId" placeholder="请选择农机" :disabled="!form.farmId">
                 <el-option v-for="item in machinesList" :key="item.id" :label="item.machineName" :value="item.id">
                 </el-option>
@@ -192,11 +182,6 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="作业地块" prop="fieldId">
-              <!-- <el-select v-model="form.fieldName" placeholder="请选择作业地块" style="width: 100%" :disabled="!form.farmName">
-                <el-option v-for="field in filteredFieldOptions" :key="field.value" :label="field.label"
-                  :value="field.value" />
-              </el-select> -->
-
               <el-select v-model="form.fieldId" placeholder="请选择作业地块" :disabled="!form.farmId">
                 <el-option v-for="item in fields" :key="item.id" :label="item.fieldName" :value="item.id">
                 </el-option>
@@ -208,12 +193,6 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="操作员" prop="operatorId">
-              <!-- <el-select v-model="form.operatorName" placeholder="请选择操作员" filterable style="width: 100%"
-                :disabled="!form.farmName">
-                <el-option v-for="operator in filteredOperatorOptions" :key="operator.value" :label="operator.label"
-                  :value="operator.value" />
-              </el-select> -->
-
               <el-select v-model="form.operatorId" placeholder="请选择操作员" :disabled="!form.farmId">
                 <el-option v-for="item in userName" :key="item.userId" :label="item.nickName" :value="item.userId">
                 </el-option>
@@ -280,7 +259,7 @@
 </template>
 
 <script>
-import { listMachineWorkRecords, getMachineWorkRecords, delMachineWorkRecords, addMachineWorkRecords, updateMachineWorkRecords, getMachineNameFieldUser } from "@/api/base/machineWorkRecords"
+import { listMachineWorkRecords, getMachineWorkRecords, delMachineWorkRecords, addMachineWorkRecords, updateMachineWorkRecords, getMachineNameFieldUser, MachineNameFieldUserText } from "@/api/base/machineWorkRecords"
 import { deptTreeSelect } from "@/api/system/user";
 import { listFieldName } from "@/api/base/field";
 import Treeselect from "@riophae/vue-treeselect";
@@ -399,9 +378,7 @@ export default {
         startTime: [
           { required: true, message: "开始时间不能为空", trigger: "change" }
         ],
-        endTime: [
-          { required: true, message: "结束时间不能为空", trigger: "change" }
-        ],
+
         operatorId: [
           { required: true, message: "操作员不能为空", trigger: "change" }
         ]
@@ -421,35 +398,51 @@ export default {
   },
   methods: {
 
-    queryMachineNameFieldUserTe() {
-      const id = this.form.id;
-      if (id != undefined) {
-        console.log("queryMachineNameFieldUserTe")
-        getTasks(id).then(response => {
-          this.form.fieldId = response.data.plotId
-          this.form.operatorId = response.data.assigneeId
-        })
-      }
-    },
-    
+
 
     queryMachineNameFieldUser(farmId) {
       console.log(this.form)
       console.log("queryMachineNameFieldUser")
-      this.form.plotId = null;
-      this.form.assigneeId = null;
-      if (farmId != undefined) {
+
+      // 清空之前的选择
+      this.form.fieldId = null;
+      this.form.machineId = null;
+      this.form.operatorId = null;
+
+      if (farmId != undefined && farmId != null) {
         const data = {
           deptId: farmId
         }
         getMachineNameFieldUser(data).then(response => {
+          console.log("回来了")
           this.fields = response.data.fields
           this.machinesList = response.data.machinesList
           this.userName = response.data.users
         })
+      } else {
+        // 如果没有选择农场,清空相关列表
+        this.fields = []
+        this.machinesList = []
+        this.userName = []
+      }
+    },
+
+    queryMachineNameFieldUserTe() {
+      const id = this.form.id;
+      console.log(id)
+      if (id != undefined) {
+        console.log("queryMachineNameFieldUserTe")
+        getMachineWorkRecords(id).then(response => {
+          this.form.fieldId = response.data.fieldId
+          this.form.machineId = response.data.machineId
+          this.form.operatorId = response.data.operatorId
+        })
       }
     },
 
+
+
+
     /** 查询地块名称 **/
     getFieldNameList() {
       listFieldName().then(res => {
@@ -677,12 +670,18 @@ export default {
         createTime: null,
         updateBy: null,
         updateTime: null,
-        remark: null
+        remark: null,
+        fieldId: null,     // 改为null而不是[]
+        machineId: null,   // 改为null而不是[]
+        operatorId: null,  // 改为null而不是[]
       }
       // 清空过滤选项
       this.filteredMachineOptions = []
       this.filteredFieldOptions = []
       this.filteredOperatorOptions = []
+      this.fields = []      // 清空地块列表
+      this.machinesList = [] // 清空农机列表
+      this.userName = []     // 清空操作员列表
       this.resetForm("form")
     },
     /** 搜索按钮操作 */
@@ -715,53 +714,32 @@ export default {
     /** 修改按钮操作 */
     handleUpdate(row) {
       this.reset()
-
-      // 直接使用行数据进行回显(因为目前使用模拟数据)
-      this.form = { ...row }
-
-      // 确保所有字段都正确回显
-      this.form.workOrderId = row.workOrderId || row.id
-      this.form.machineCode = row.machineCode
-      this.form.machineName = row.machineName
-      this.form.machineType = row.machineType
-      this.form.farmName = row.farmName
-      this.form.fieldName = row.fieldName
-      this.form.taskName = row.taskName
-      this.form.taskType = row.taskType
-      this.form.status = row.status
-      this.form.startTime = row.startTime
-      this.form.endTime = row.endTime
-      this.form.operatorName = row.operatorName
-      this.form.remark = row.remark
-
-      // 编辑时根据农场初始化过滤选项(传递初始化标志,保持原有值)
-      if (this.form.farmName) {
-        this.handleFarmChange(this.form.farmName, true)
-      }
-
-      this.open = true
-      this.title = "修改农机作业记录"
-
-      // 如果将来需要使用API,可以使用以下代码
-      /*
       const id = row.id || this.ids
+
+      // 先获取记录详情
       getMachineWorkRecords(id).then(response => {
         this.form = response.data
-        // 确保保留原始作业单号
-        if (!this.form.workOrderId) {
-          this.form.workOrderId = row.workOrderId
-        }
-        // 编辑时根据农场初始化过滤选项
-        if (this.form.farmName) {
-          this.handleFarmChange(this.form.farmName)
+
+        // 如果有农场ID,则获取对应的农机、地块和操作员数据
+        if (this.form.farmId != undefined) {
+          const data = {
+            deptId: this.form.farmId
+          }
+          MachineNameFieldUserText(data).then(response => {
+            this.fields = response.data.fields
+            this.machinesList = response.data.machinesList
+            this.userName = response.data.users
+
+            // 设置打开对话框和标题
+            this.open = true
+            this.title = "修改农机作业记录"
+          })
+        } else {
+          // 如果没有农场ID也打开对话框
+          this.open = true
+          this.title = "修改农机作业记录"
         }
-        this.open = true
-        this.title = "修改农机作业记录"
-      }).catch(error => {
-        console.warn('API调用失败,使用当前行数据:', error)
-        // 回退到使用行数据的逻辑
       })
-      */
     },
     /** 提交按钮 */
     submitForm() {

+ 1032 - 0
src/views/base/speciesInfo/index.vue

@@ -0,0 +1,1032 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="奶牛编号" prop="speciesId">
+        <el-input v-model="queryParams.speciesId" placeholder="请输入奶牛编号" clearable @keyup.enter.native="handleQuery" />
+      </el-form-item>
+      <el-form-item label="品种" prop="breed">
+        <el-input v-model="queryParams.breed" placeholder="请输入品种" clearable @keyup.enter.native="handleQuery" />
+      </el-form-item>
+      <!-- <el-form-item label="物种类型" prop="speciesType">
+        <el-select v-model="queryParams.speciesType" placeholder="请选择物种类型" clearable>
+          <el-option
+            v-for="dict in dict.type.species_type"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item> -->
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="queryParams.sex" placeholder="请选择性别" clearable>
+          <el-option v-for="dict in dict.type.species_sex" :key="dict.value" :label="dict.label" :value="dict.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="年龄" prop="age">
+        <el-input v-model="queryParams.age" placeholder="请输入年龄" clearable @keyup.enter.native="handleQuery" />
+      </el-form-item>
+      <!-- <el-form-item label="体重(kg)" prop="weight">
+        <el-input v-model="queryParams.weight" placeholder="请输入体重(kg)" clearable @keyup.enter.native="handleQuery" />
+      </el-form-item> -->
+      <!-- <el-form-item label="所属农场" prop="farmId">
+        <el-input
+          v-model="queryParams.farmId"
+          placeholder="请输入所属农场"
+          clearable
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item> -->
+
+      <el-form-item label="所属农场" prop="deptIdList">
+        <treeselect v-model="queryParams.deptIdList" :options="deptOptions" multiple :flat="true" :limit="1"
+          :limitText="count => `+${count}`" style="width:200px" :show-count="true" placeholder="请选所属农场" />
+      </el-form-item>
+      <el-form-item label="所属地块" prop="fieldIdList">
+        <el-select v-model="queryParams.fieldIdList" placeholder="请选择地块" multiple collapse-tags filterable clearable>
+          <el-option v-for="dict in fieldIdList" :key="dict.value" :label="dict.label" :value="dict.value" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="健康状态" prop="healthStatus">
+        <el-select v-model="queryParams.healthStatus" placeholder="请选择健康状态" clearable>
+          <el-option v-for="dict in dict.type.health_status" :key="dict.value" :label="dict.label"
+            :value="dict.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="监测温度" prop="latestTempArray">
+        <div style="display: flex; align-items: center; gap: 8px;">
+          <el-input v-model="latestTempArray.latestTempMin" placeholder="最低温度" clearable style="width: 120px;"
+            @keyup.enter.native="handleQuery" />
+          <span>-</span>
+          <el-input v-model="latestTempArray.latestTempMax" placeholder="最高温度" clearable style="width: 120px;"
+            @keyup.enter.native="handleQuery" />
+        </div>
+      </el-form-item>
+
+      <el-form-item label="监测时间" prop="latestTempTime">
+        <el-date-picker v-model="queryParams.latestTempTimes" type="datetimerange" range-separator="至"
+          start-placeholder="开始时间" end-placeholder="结束时间" value-format="yyyy-MM-dd HH:mm:ss" clearable />
+      </el-form-item>
+
+
+
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+
+    <!-- 统计面板 -->
+    <div class="statistics-panel">
+      <el-row :gutter="12">
+        <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
+          <div class="stat-card published-card">
+            <div class="stat-content">
+              <div class="stat-title">异常状态数</div>
+              <div class="stat-number">{{ statistics.exceptionCount || 0 }}</div>
+              <div class="stat-desc">异常数量</div>
+            </div>
+          </div>
+        </el-col>
+        <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
+          <!-- <div class="stat-card tech-card">
+            <div class="stat-content">
+              <div class="stat-title">待处理建议数</div>
+              <div class="stat-number">{{ statistics.pendingCount || 0 }}</div>
+              <div class="stat-desc">建议数量</div>
+            </div>
+          </div> -->
+        </el-col>
+        <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
+        </el-col>
+        <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
+        </el-col>
+        <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
+        </el-col>
+      </el-row>
+    </div>
+
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
+          v-hasPermi="['base:speciesInfo:add']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate"
+          v-hasPermi="['base:speciesInfo:edit']">修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete"
+          v-hasPermi="['base:speciesInfo:remove']">删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
+          v-hasPermi="['base:speciesInfo:export']">导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="speciesInfoList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <!-- <el-table-column label="主键ID" align="center" prop="id" /> -->
+      <el-table-column label="奶牛编号" align="center" prop="speciesId" />
+      <el-table-column label="品种" align="center" prop="breed" />
+
+      <el-table-column label="性别" align="center" prop="sex">
+        <template slot-scope="scope">
+          <dict-tag :options="dict.type.species_sex" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="年龄" align="center" prop="age" />
+      <el-table-column label="体重(kg)" align="center" prop="weight" />
+      <el-table-column label="所属农场" align="center" prop="farmName" />
+      <el-table-column label="所属地块" align="center" prop="fieldName" />
+      <el-table-column label="最近监测温度" align="center" prop="latestTemp" />
+      <el-table-column label="最近监测时间" align="center" prop="latestTempTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.latestTempTime, '{y}-{m}-{d}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="健康状态" align="center" prop="healthStatus">
+        <template slot-scope="scope">
+          <dict-tag :options="dict.type.health_status" :value="scope.row.healthStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" />
+
+      <el-table-column label="缩略图" align="center" width="120">
+        <template slot-scope="scope">
+          <el-image 
+            v-if="scope.row.thumbnailUrl" 
+            :src="scope.row.thumbnailUrl"
+            :preview-src-list="[scope.row.thumbnailUrl]"
+            style="width: 80px; height: 80px; object-fit: cover; cursor: pointer;" 
+            fit="cover"
+            :hide-on-click-modal="true"
+          >
+            <div slot="error" class="image-slot">
+              <i class="el-icon-video-play"></i>
+            </div>
+          </el-image>
+          <div v-else class="thumbnail-placeholder">
+            <i class="el-icon-video-play"></i>
+          </div>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-video-play" @click="handleViewVideo(scope.row)"
+            v-hasPermi="['base:speciesInfo:view']">查看视频</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+            v-hasPermi="['base:speciesInfo:edit']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+            v-hasPermi="['base:speciesInfo:remove']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
+      @pagination="getList" />
+
+    <!-- 添加或修改物种基础信息对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="奶牛编号" prop="speciesId">
+          <el-input v-model="form.speciesId" placeholder="请输入物种编号" />
+        </el-form-item>
+        <el-form-item label="品种" prop="breed">
+          <el-input v-model="form.breed" placeholder="请输入品种" />
+        </el-form-item>
+        <el-form-item label="性别" prop="sex">
+          <el-select v-model="form.sex" placeholder="请选择性别">
+            <el-option v-for="dict in dict.type.species_sex" :key="dict.value" :label="dict.label"
+              :value="dict.value"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="年龄" prop="age">
+          <el-input v-model="form.age" placeholder="请输入年龄" />
+        </el-form-item>
+        <el-form-item label="体重(kg)" prop="weight">
+          <el-input v-model="form.weight" placeholder="请输入体重(kg)" />
+        </el-form-item>
+
+
+        <el-form-item label="所属农场" prop="farmId">
+          <treeselect v-model="form.farmId" :options="enabledDeptOptions"
+            @input="queryMachineNameFieldUser(form.farmId)" placeholder="请先选择所属农场" />
+        </el-form-item>
+        <el-form-item label="所属地块" prop="fieldId"> <!-- 修改这里 -->
+          <el-select v-model="form.fieldId" placeholder="请选择作业地块" :disabled="!form.farmId || loadingFields"
+            @change="handleFieldChange">
+            <el-option v-for="item in fields" :key="item.id" :label="item.fieldName" :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="健康状态" prop="healthStatus">
+          <el-radio-group v-model="form.healthStatus">
+            <el-radio v-for="dict in dict.type.health_status" :key="dict.value" :label="parseInt(dict.value)">{{
+              dict.label
+            }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+
+    <!-- 视频播放对话框 -->
+    <el-dialog title="视频播放" :visible.sync="videoVisible" width="800px" append-to-body @closed="closeVideo">
+      <div class="video-dialog-content">
+        <!-- Tab 切换 -->
+        <el-tabs v-model="activeVideoTab" @tab-click="handleTabClick">
+          <el-tab-pane label="正常视频" name="normal">
+            <div class="video-container">
+              <video ref="videoPlayerNormal" :src="normalVideoUrl" controls autoplay style="width: 100%; height: 100%;">
+                您的浏览器不支持视频播放
+              </video>
+            </div>
+          </el-tab-pane>
+          <el-tab-pane label="感温视频" name="thermal">
+            <div class="video-container">
+              <video ref="videoPlayerThermal" :src="thermalVideoUrl" controls autoplay
+                style="width: 100%; height: 100%;">
+                您的浏览器不支持视频播放
+              </video>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="videoVisible = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+
+     
+  </div>
+</template>
+
+<script>
+import { getMachineNameFieldUser, MachineNameFieldUserText } from "@/api/base/machineWorkRecords"
+import { listSpeciesInfo, getSpeciesInfo, delSpeciesInfo, addSpeciesInfo, updateSpeciesInfo, getCount } from "@/api/base/speciesInfo"
+import { deptTreeSelect } from "@/api/system/user";
+import { listFieldName } from "@/api/base/field"
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+export default {
+  name: "SpeciesInfo",
+  components: { Treeselect },
+  dicts: ['species_type', 'health_status', 'species_sex'],
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 物种基础信息表格数据
+      speciesInfoList: [],
+      // 弹出层标题
+      title: "",
+      // 部门树选项
+      deptOptions: undefined,
+      // 所属地块
+      fieldIdList: [],
+      // 过滤掉已禁用部门树选项
+      enabledDeptOptions: undefined,
+      // 是否显示弹出层
+      open: false,
+      fields: [],
+      userName: [],
+      loadingFields: false, // 新增字段,用于控制地块加载状态
+      // 视频相关
+      videoVisible: false,
+      activeVideoTab: 'normal', // 默认显示正常视频
+      normalVideoUrl: 'https://azn.gbdfarm.com/statics/2026/03/26/cs1235_20260326172707A034.mp4',
+      thermalVideoUrl: 'https://azn.gbdfarm.com/statics/2026/03/26/add1_20260326174603A035.mp4',
+
+  
+
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        speciesId: null,
+        breed: null,
+        speciesType: null,
+        sex: null,
+        age: null,
+        weight: null,
+        farmId: null,
+        fieldId: null,
+        latestTemp: null,
+        latestTempTimes: null,
+        healthStatus: null,
+        deptIdList: [],
+      },
+      latestTempArray: {
+        latestTempMin: null,
+        latestTempMax: null,
+      },
+      // 统计数据
+      statistics: {
+        exceptionCount: 0,
+
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        speciesId: [
+          { required: true, message: "物种编号不能为空", trigger: "blur" }
+        ],
+        breed: [
+          { required: true, message: "品种不能为空", trigger: "blur" }
+        ],
+        /* speciesType: [
+          { required: true, message: "物种类型不能为空", trigger: "change" }
+        ], */
+        farmId: [
+          { required: true, message: "所属农场不能为空", trigger: "blur" }
+        ],
+        fieldId: [
+          { required: true, message: "所属地块不能为空", trigger: "blur" }
+        ],
+        healthStatus: [
+          { required: true, message: "健康状态不能为空", trigger: "change" }
+        ],
+        createTime: [
+          { required: true, message: "创建时间不能为空", trigger: "blur" }
+        ],
+      }
+    }
+  },
+  created() {
+    this.getList()
+    this.getDeptTree();
+    this.getFieldNameList();
+    this.getStatistics();
+  },
+  methods: {
+
+   
+
+     /** 查看视频 */
+    handleViewVideo(row) {
+      this.activeVideoTab = 'normal'
+      this.videoVisible = true
+
+      this.$nextTick(() => {
+        if (this.$refs.videoPlayerNormal) {
+          this.$refs.videoPlayerNormal.play()
+        }
+      })
+    },
+
+    /** 查看视频 */
+    handleViewVideo(row) {
+      this.activeVideoTab = 'normal' // 重置为正常视频
+      this.videoVisible = true
+
+      // 确保视频在弹窗打开后自动播放
+      this.$nextTick(() => {
+        if (this.$refs.videoPlayerNormal) {
+          this.$refs.videoPlayerNormal.play()
+        }
+      })
+    },
+
+    /** Tab 切换处理 */
+    handleTabClick(tab) {
+      // 切换 Tab 时暂停当前视频,播放新 Tab 的视频
+      this.$nextTick(() => {
+        if (tab.name === 'normal' && this.$refs.videoPlayerNormal) {
+          if (this.$refs.videoPlayerThermal) {
+            this.$refs.videoPlayerThermal.pause()
+          }
+          this.$refs.videoPlayerNormal.play()
+        } else if (tab.name === 'thermal' && this.$refs.videoPlayerThermal) {
+          if (this.$refs.videoPlayerNormal) {
+            this.$refs.videoPlayerNormal.pause()
+          }
+          this.$refs.videoPlayerThermal.play()
+        }
+      })
+    },
+
+    /** 关闭视频 */
+    closeVideo() {
+      // 暂停所有视频并清空地址
+      if (this.$refs.videoPlayerNormal) {
+        this.$refs.videoPlayerNormal.pause()
+      }
+      if (this.$refs.videoPlayerThermal) {
+        this.$refs.videoPlayerThermal.pause()
+      }
+      this.normalVideoUrl = ''
+      this.thermalVideoUrl = ''
+
+      // 重置 URL(延迟一下再设置,避免闪烁)
+      this.$nextTick(() => {
+        this.normalVideoUrl = 'https://azn.gbdfarm.com/statics/2026/03/26/cs1235_20260326172707A034.mp4'
+        this.thermalVideoUrl = 'https://azn.gbdfarm.com/statics/2026/03/26/add1_20260326174603A035.mp4'
+      })
+    },
+
+    /** 获取统计数据 */
+    getStatistics() {
+      const params = {
+        speciesType: 0,
+        healthStatus: 2
+      }
+
+      getCount(params).then(response => {
+        this.statistics.exceptionCount = response.data || []
+      }).catch(() => {
+        this.statistics = {
+          exceptionCount: 0,
+
+        }
+      })
+    },
+
+    handleFieldChange(value) {
+      // 当用户选择地块时,确保选中的地块属于当前农场
+      if (value && !this.fields.find(field => field.id === value)) {
+        // 如果选中的地块不在当前农场的地块列表中,清空选中
+        this.form.fieldId = null;
+        this.$message.warning('该地块不属于当前农场');
+      }
+    },
+
+    queryMachineNameFieldUser(farmId) {
+      // 清空地块列表,避免选择不同农场时出现残留数据
+      this.fields = [];
+
+      // 如果没有农场 ID,直接返回
+      if (!farmId) {
+        return;
+      }
+
+      // 设置 loading 状态(可选)
+      this.loadingFields = true;
+
+      const data = {
+        deptId: farmId
+      }
+
+      getMachineNameFieldUser(data).then(response => {
+        this.fields = response.data.fields || [];
+
+        // 如果响应中有默认地块,设置为选中状态
+        if (response.data.defaultFieldId) {
+          this.form.fieldId = response.data.defaultFieldId;
+        } else if (this.form.fieldId && !this.fields.find(field => field.id === this.form.fieldId)) {
+          // 如果当前选中的地块不在新农场的地块列表中,清空选中
+          this.form.fieldId = null;
+        }
+
+        // 关闭 loading 状态
+        this.loadingFields = false;
+      }).catch(() => {
+        this.loadingFields = false;
+      });
+    },
+    queryMachineNameFieldUserTe() {
+      const id = this.form.id;
+      if (id != undefined) {
+        getSpeciesInfo(id).then(response => {
+          console.log("打开框")
+          console.log(response.data.fieldId)
+          this.form.fieldId = response.data.fieldId
+        })
+      }
+    },
+
+    /** 查询地块名称 **/
+    getFieldNameList() {
+      listFieldName().then(res => {
+        this.fieldIdList = res;
+        this.fieldIdList.forEach(x => {
+          x.value = x.id;
+          x.label = x.fieldName;
+        })
+      });
+    },
+
+    /** 查询部门下拉树结构 */
+    getDeptTree() {
+      deptTreeSelect().then(response => {
+        this.deptOptions = response.data;
+        this.enabledDeptOptions = this.filterDisabledDept(JSON.parse(JSON.stringify(response.data)))
+      });
+    },
+    filterDisabledDept(deptList) {
+      return deptList.filter(dept => {
+        if (dept.disabled) {
+          return false
+        }
+        if (dept.children && dept.children.length) {
+          dept.children = this.filterDisabledDept(dept.children)
+        }
+        return true
+      })
+    },
+    /** 查询物种基础信息列表 */
+    getList() {
+      this.loading = true
+      listSpeciesInfo(this.queryParams).then(response => {
+        this.speciesInfoList = response.rows
+        this.total = response.total
+        
+        // 为每行数据生成视频缩略图
+        this.$nextTick(() => {
+          this.generateThumbnails()
+        })
+        
+        this.loading = false
+      })
+    },
+
+    /** 生成视频缩略图 */
+    generateThumbnails() {
+      console.log("生成视频缩略图")
+      this.speciesInfoList.forEach((row, index) => {
+        
+          this.extractVideoFrame('https://azn.gbdfarm.com/statics/2026/03/26/cs1235_20260326172707A034.mp4', index)
+        
+      })
+
+    },
+
+     
+    /** 提取视频第一帧 */
+    extractVideoFrame(videoUrl, rowIndex) {
+      const video = document.createElement('video')
+      video.src = videoUrl
+      video.crossOrigin = 'anonymous'
+      video.muted = true
+      
+      
+      
+      video.addEventListener('loadeddata', () => {
+        video.currentTime = 0.1
+      })
+      
+      video.addEventListener('seeked', () => {
+        const canvas = document.createElement('canvas')
+        canvas.width = video.videoWidth || 320
+        canvas.height = video.videoHeight || 240
+        
+        const ctx = canvas.getContext('2d')
+        ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
+        
+        const thumbnailUrl = canvas.toDataURL('image/jpeg', 0.8)
+        
+        // 使用 Vue.set 确保响应式更新
+        console.log(thumbnailUrl)
+        this.$set(this.speciesInfoList[rowIndex], 'thumbnailUrl', thumbnailUrl)
+        
+        // 清理
+        setTimeout(() => {
+          video.src = ''
+          video.load()
+          canvas.remove()
+        }, 100)
+      })
+      
+      video.load()
+    },
+    
+    // 新增:处理图片加载错误
+    handleImageError(event) {
+      console.log('图片加载失败:', event)
+      // 可以在这里添加备用图片或显示错误信息
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false
+      this.reset()
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        speciesId: null,
+        breed: null,
+        speciesType: null,
+        sex: null,
+        age: null,
+        weight: null,
+        farmId: null,
+        fieldId: null,
+        latestTemp: null,
+        latestTempTime: null,
+        latestTempTimes: null,
+        healthStatus: null,
+        createBy: null,
+        createTime: null,
+        updateBy: null,
+        updateTime: null,
+        remark: null,
+        latestTempArray: null
+      }
+      this.resetForm("form")
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.queryParams.params = {}
+      // 处理时间范围查询
+      if (this.queryParams.latestTempTimes && this.queryParams.latestTempTimes.length === 2) {
+        /* this.queryParams.latestTempTimeStart = this.queryParams.latestTempTimes[0]
+        this.queryParams.latestTempTimeEnd = this.queryParams.latestTempTimes[1] */
+
+        this.queryParams.params["latestTempTimeStart"] = this.queryParams.latestTempTimes[0]
+        this.queryParams.params["latestTempTimeEnd"] = this.queryParams.latestTempTimes[1]
+      } else {
+        this.queryParams.latestTempTimeStart = null
+        this.queryParams.latestTempTimeEnd = null
+      }
+      // 检查监测温度范围是否完整且有效
+      if (this.latestTempArray.latestTempMin !== null && this.latestTempArray.latestTempMax === null) {
+        this.$message.error('请填写监测温度最大值');
+        return;
+      }
+      if (this.latestTempArray.latestTempMin === null && this.latestTempArray.latestTempMax !== null) {
+        this.$message.error('请填写监测温度最小值');
+        return;
+      }
+      if (this.latestTempArray.latestTempMin !== null && this.latestTempArray.latestTempMax !== null) {
+        if (this.latestTempArray.latestTempMin > this.latestTempArray.latestTempMax) {
+          this.$message.error('监测温度最小值不能大于最大值');
+          return;
+        }
+      }
+      /* this.queryParams.latestTempMin = this.latestTempArray.latestTempMin
+      this.queryParams.latestTempMax = this.latestTempArray.latestTempMax */
+      this.queryParams.params["latestTempMin"] = this.latestTempArray.latestTempMin
+      this.queryParams.params["latestTempMax"] = this.latestTempArray.latestTempMax
+      this.getList()
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm")
+      this.queryParams.latestTempTimes = []
+      this.latestTempArray = {
+        latestTempMin: null,
+        latestTempMax: null,
+      }
+      this.queryParams.latestTempMin = null,
+        this.queryParams.latestTempMax = null,
+        this.handleQuery()
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset()
+      this.open = true
+      this.title = "添加物种基础信息"
+    },
+    /** 修改按钮操作 */
+    /* handleUpdate(row) {
+      if (row.farmId != undefined) {
+        const data = {
+          deptId: row.farmId
+        }
+        MachineNameFieldUserText(data).then(response => {
+          console.log("修改按钮")
+          console.log(response.data.fields)
+          this.fields = response.data.fields
+        })
+      }
+      this.reset()
+      const id = row.id || this.ids
+      getSpeciesInfo(id).then(response => {
+        this.form = response.data
+        this.open = true
+        this.title = "修改物种基础信息"
+      })
+    }, */
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset()
+      const id = row.id || this.ids
+
+      // 先获取详细信息
+      getSpeciesInfo(id).then(response => {
+        this.form = response.data
+
+        // 临时存储原始的 fieldId 值
+        const originalFieldId = this.form.fieldId
+
+        // 如果有农场 ID,则获取对应的地块数据
+        if (this.form.farmId != undefined && this.form.farmId != null) {
+          const data = {
+            deptId: this.form.farmId
+          }
+          MachineNameFieldUserText(data).then(response => {
+            this.fields = response.data.fields
+
+            // 使用 $nextTick 确保 DOM 更新后再设置 fieldId
+            this.$nextTick(() => {
+              // 优先使用 API 返回的 fieldId,否则使用原始值
+              this.form.fieldId = response.data.fieldId || originalFieldId
+            })
+
+            // 直接打开对话框
+            this.open = true
+            this.title = "修改物种基础信息"
+          })
+        } else {
+          // 如果没有农场 ID 也打开对话框
+          this.open = true
+          this.title = "修改物种基础信息"
+        }
+      })
+    },
+
+
+
+
+    /** 提交按钮 */
+    submitForm() {
+      this.form.speciesType = 0
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateSpeciesInfo(this.form).then(response => {
+              this.$modal.msgSuccess("修改成功")
+              this.open = false
+              this.getList()
+              this.getStatistics();
+            })
+          } else {
+            addSpeciesInfo(this.form).then(response => {
+              this.$modal.msgSuccess("新增成功")
+              this.open = false
+              this.getList()
+              this.getStatistics();
+            })
+          }
+        }
+      })
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids
+      this.$modal.confirm('是否确认删除物种基础信息编号为"' + ids + '"的数据项?').then(function () {
+        return delSpeciesInfo(ids)
+      }).then(() => {
+        this.getList()
+        this.getStatistics();
+        this.$modal.msgSuccess("删除成功")
+      }).catch(() => { })
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      this.download('base/speciesInfo/export', {
+        ...this.queryParams
+      }, `speciesInfo_${new Date().getTime()}.xlsx`)
+    }
+  }
+}
+</script>
+<style lang="scss">
+
+
+/* 图片插槽样式 */
+.image-slot {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  font-size: 24px;
+}
+
+/* 缩略图占位符样式 */
+.thumbnail-placeholder {
+  width: 80px;
+  height: 80px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: white;
+  font-size: 24px;
+  border-radius: 4px;
+}
+/* 视频缩略图样式 */
+.video-thumbnail {
+  position: relative;
+  width: 100px;
+  height: 75px;
+  border-radius: 4px;
+  overflow: hidden;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  transition: all 0.3s;
+  
+  &:hover {
+    transform: scale(1.05);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  }
+  
+  .thumbnail-image {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+  }
+  
+  .thumbnail-placeholder {
+    width: 100%;
+    height: 100%;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: white;
+    font-size: 24px;
+  }
+}
+
+/* 缩略图预览对话框样式 */
+.thumbnail-preview-dialog {
+  .el-dialog__body {
+    padding: 20px;
+    text-align: center;
+  }
+  
+  .thumbnail-preview-content {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    min-height: 400px;
+    background: #f5f5f5;
+    border-radius: 8px;
+    
+    .preview-image {
+      max-width: 100%;
+      max-height: 600px;
+      object-fit: contain;
+      border-radius: 4px;
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    }
+  }
+}
+/* 统计面板样式 */
+.statistics-panel {
+  margin-bottom: 16px;
+  padding: 20px;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+}
+
+.statistics-panel .el-row {
+  display: flex;
+  justify-content: space-between;
+}
+
+.statistics-panel .el-col {
+  flex: 1;
+  max-width: none !important;
+}
+
+.stat-card {
+  border-radius: 12px;
+  padding: 16px;
+  height: 120px;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  transition: all 0.3s ease;
+  cursor: pointer;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.05);
+  position: relative;
+  overflow: hidden;
+  color: #fff;
+  margin-bottom: 10px;
+}
+
+.stat-card:hover {
+  transform: translateY(-3px);
+  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15), 0 4px 10px rgba(0, 0, 0, 0.1);
+}
+
+
+.stat-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: flex-start;
+}
+
+.stat-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: rgba(255, 255, 255, 0.9);
+  margin-bottom: 0;
+  line-height: 1.2;
+}
+
+.stat-number {
+  font-size: 32px;
+  font-weight: 700;
+  line-height: 1.0;
+  margin: 6px 0;
+  color: #fff;
+}
+
+.stat-desc {
+  font-size: 11px;
+  font-weight: 500;
+  color: rgba(255, 255, 255, 0.85);
+  line-height: 1.2;
+  margin: 0;
+}
+
+/* 不同卡片的主题色 */
+.total-card {
+  background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+}
+
+.tech-card {
+  background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
+}
+
+.policy-card {
+  background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
+}
+
+.published-card {
+  background: linear-gradient(135deg, #ec4899 0%, #f472b6 100%);
+}
+
+.draft-card {
+  background: linear-gradient(135deg, #06b6d4 0%, #38bdf8 100%);
+}
+
+/* 视频播放器样式 */
+.video-container {
+  width: 100%;
+  height: 450px;
+  background: #000;
+  border-radius: 8px;
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.video-container video {
+  max-width: 100%;
+  max-height: 100%;
+  outline: none;
+}
+
+/* 视频对话框内容样式 */
+.video-dialog-content {
+  padding: 10px 0;
+}
+
+.video-dialog-content .el-tabs__content {
+  padding: 0;
+}
+
+.video-dialog-content .el-tabs__item {
+  font-size: 14px;
+  font-weight: 500;
+}
+
+/* 调整 Tab 栏样式 */
+.video-dialog-content ::v-deep .el-tabs__header {
+  margin-bottom: 16px;
+}
+
+.video-dialog-content ::v-deep .el-tabs__nav-wrap::after {
+  height: 2px;
+}
+</style>

+ 107 - 41
src/views/base/tasks/index.vue

@@ -30,15 +30,13 @@
       </el-form-item>
 
       <el-form-item label="执行时间" prop="executeTimeRange">
-        <el-date-picker v-model="executeTimeRange" type="datetimerange" range-separator="至"
-          start-placeholder="开始时间" end-placeholder="结束时间" value-format="yyyy-MM-dd HH:mm:ss" clearable
-          style="width: 350px">
+        <el-date-picker v-model="executeTimeRange" type="datetimerange" range-separator="至" start-placeholder="开始时间"
+          end-placeholder="结束时间" value-format="yyyy-MM-dd HH:mm:ss" clearable style="width: 350px">
         </el-date-picker>
       </el-form-item>
       <el-form-item label="完成时间" prop="completionTimeRange">
-        <el-date-picker v-model="completionTimeRange" type="datetimerange" range-separator="至"
-          start-placeholder="开始时间" end-placeholder="结束时间" value-format="yyyy-MM-dd HH:mm:ss" clearable
-          style="width: 350px">
+        <el-date-picker v-model="completionTimeRange" type="datetimerange" range-separator="至" start-placeholder="开始时间"
+          end-placeholder="结束时间" value-format="yyyy-MM-dd HH:mm:ss" clearable style="width: 350px">
         </el-date-picker>
       </el-form-item>
       <el-form-item>
@@ -77,11 +75,17 @@
       <el-table-column label="所属地块" align="center" prop="fieldName" />
       <el-table-column label="所属农场" align="center" prop="deptName" />
       <el-table-column label="执行人" align="center" prop="assigneeName" />
-      <el-table-column label="执行时间" align="center" prop="executeTime" width="180">
+      <el-table-column label="计划执行时间" align="center" prop="executeTime" width="180">
         <template slot-scope="scope">
           <span>{{ parseTime(scope.row.executeTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
         </template>
       </el-table-column>
+      <el-table-column label="计划完成时间" align="center" prop="plannedCompletetime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.plannedCompletetime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+        </template>
+      </el-table-column>
+
       <!-- <el-table-column label="任务状态" align="center" prop="taskStatus" width="100">
         <template slot-scope="scope">
           <el-tag :type="scope.row.taskStatus === 1 ? 'success' : 'warning'" size="small">
@@ -97,7 +101,14 @@
       </el-table-column>
 
       <el-table-column label="任务说明" align="center" prop="remark" width="150" show-overflow-tooltip />
-      <el-table-column label="完成时间" align="center" prop="completionTime" width="180">
+      <el-table-column label="实际开始时间" align="center" prop="practicalBegintime" width="180">
+        <template slot-scope="scope">
+          <span>{{ scope.row.practicalBegintime ? parseTime(scope.row.practicalBegintime, '{y}-{m}-{d} {h}:{i}:{s}') :
+            '-'
+            }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="实际完成时间" align="center" prop="completionTime" width="180">
         <template slot-scope="scope">
           <span>{{ scope.row.completionTime ? parseTime(scope.row.completionTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-'
           }}</span>
@@ -126,8 +137,8 @@
       @pagination="getList" />
 
     <!-- 添加或修改农事任务对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body @opened="queryMachineNameFieldUserTe()">
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+    <el-dialog :title="title" :visible.sync="open" width="650px" append-to-body @opened="queryMachineNameFieldUserTe()">
+      <el-form ref="form" :model="form" :rules="rules" label-width="110px">
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="任务名称" prop="taskName">
@@ -174,16 +185,26 @@
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="12">
+          <!-- <el-col :span="12">
             <el-form-item label="执行时间" prop="executeTime">
               <el-date-picker clearable v-model="form.executeTime" type="datetime" placeholder="请选择执行时间"
                 style="width: 100%">
               </el-date-picker>
             </el-form-item>
+          </el-col> -->
+
+          <el-col :span="12">
+            <el-form-item label="任务状态" prop="taskStatus">
+              <el-select v-model="form.taskStatus" placeholder="请选择任务状态" style="width: 100%">
+                <el-option v-for="dict in dict.type.task_status" :key="dict.value" :label="dict.label"
+                  :value="dict.value"></el-option>
+              </el-select>
+            </el-form-item>
+
           </el-col>
         </el-row>
         <el-row :gutter="20">
-          <el-col :span="12">
+          <!-- <el-col :span="12">
             <el-form-item label="任务状态" prop="taskStatus">
               <el-select v-model="form.taskStatus" placeholder="请选择任务状态" style="width: 100%">
                 <el-option v-for="dict in dict.type.task_status" :key="dict.value" :label="dict.label"
@@ -191,27 +212,51 @@
               </el-select>
             </el-form-item>
 
-          </el-col>
+          </el-col> -->
           <el-col :span="12">
             <el-form-item label="作物名称" prop="growCrops">
               <el-input v-model="form.growCrops" placeholder="请输入作物名称" />
             </el-form-item>
           </el-col>
+          <el-col :span="12">
+            <el-form-item label="计划开始时间" prop="executeTime">
+              <el-date-picker clearable v-model="form.executeTime" type="datetime" placeholder="请选择计划开始时间"
+                style="width: 100%">
+              </el-date-picker>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+
+          <el-col :span="12">
+            <el-form-item label="计划完成时间" prop="plannedCompletetime">
+              <el-date-picker clearable v-model="form.plannedCompletetime" type="datetime" placeholder="请选择计划完成时间"
+                style="width: 100%">
+              </el-date-picker>
+            </el-form-item>
+          </el-col>
         </el-row>
         <el-form-item label="任务说明" prop="remark">
           <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入任务说明" />
         </el-form-item>
         <el-row :gutter="20" v-if="form.taskStatus == 1">
+
           <el-col :span="12">
-            <el-form-item label="完成时间" prop="completionTime">
-              <el-date-picker clearable v-model="form.completionTime"
-               type="datetime" 
-               value-format="yyyy-MM-dd HH:mm:ss"
-                placeholder="请选择完成时间" 
-                style="width: 100%">
+            <el-form-item label="实际开始时间" prop="practicalBegintime">
+              <el-date-picker clearable v-model="form.practicalBegintime" type="datetime"
+                value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择实际开始时间" style="width: 100%">
               </el-date-picker>
             </el-form-item>
           </el-col>
+          <el-col :span="12">
+            <el-form-item label="实际完成时间" prop="completionTime">
+              <el-date-picker clearable v-model="form.completionTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
+                placeholder="请选择实际完成时间" style="width: 100%">
+              </el-date-picker>
+            </el-form-item>
+          </el-col>
+
         </el-row>
         <el-form-item label="完成说明" prop="completionDesc" v-if="form.taskStatus == 1">
           <el-input v-model="form.completionDesc" type="textarea" :rows="3" placeholder="请输入完成说明" />
@@ -235,7 +280,7 @@
 </template>
 
 <script>
-import { getMachineNameFieldUser,MachineNameFieldUserText } from "@/api/base/machineWorkRecords"
+import { getMachineNameFieldUser, MachineNameFieldUserText } from "@/api/base/machineWorkRecords"
 import { listTasks, getTasks, delTasks, addTasks, updateTasks } from "@/api/base/tasks"
 import { deptTreeSelect } from "@/api/system/user";
 import { listFieldName } from "@/api/base/field"
@@ -282,7 +327,7 @@ export default {
         farmNames: [],
         executor: null,
         taskStatus: null,
-    
+
       },
       // 地块选项
       plotOptions: [],
@@ -299,7 +344,7 @@ export default {
       enabledDeptOptions: undefined,
       // 所属地块
       fieldIdList: [],
-     
+
       fields: [],
       userName: [],
       executeTimeRange: [],
@@ -324,7 +369,10 @@ export default {
           { required: true, message: "执行人不能为空", trigger: "change" }
         ],
         executeTime: [
-          { required: true, message: "执行时间不能为空", trigger: "blur" }
+          { required: true, message: "计划开始时间不能为空", trigger: "blur" }
+        ],
+        plannedCompletetime: [
+          { required: true, message: "计划完成时间不能为空", trigger: "blur" }
         ],
         taskStatus: [
           { required: true, message: "任务状态不能为空", trigger: "change" }
@@ -340,6 +388,16 @@ export default {
   },
   methods: {
 
+    formatDateTime(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');
+      const hours = String(date.getHours()).padStart(2, '0');
+      const minutes = String(date.getMinutes()).padStart(2, '0');
+      return `${year}-${month}-${day} ${hours}:${minutes}`;
+    },
+
     queryMachineNameFieldUser(farmId) {
       console.log("queryMachineNameFieldUser")
       this.form.plotId = null;
@@ -497,10 +555,12 @@ export default {
         plotId: null,
         assigneeId: null,
         executeTime: null,
+        plannedCompletetime: null,
         growCrops: null,
         remark: null,
         taskStatus: "0",
         taskDescription: null,
+        practicalBegintime: null,
         completionTime: null,
         completionDesc: null,
         field: null,
@@ -536,7 +596,7 @@ export default {
       this.open = true
       this.title = "添加农事任务"
     },
-   
+
     /** 修改按钮操作 */
     handleUpdate(row) {
       if (row.farmId != undefined) {
@@ -560,16 +620,21 @@ export default {
 
     },
     formatDateTime(dateString) {
-    const date = new Date(dateString);
-    return date.toISOString().replace('T', ' ').substring(0, 16);
-  },
+      const date = new Date(dateString);
+      return date.toISOString().replace('T', ' ').substring(0, 16);
+    },
     /** 提交按钮 */
     submitForm() {
-      console.log("提交按钮")
+      console.log("提交按钮");
       this.$refs["form"].validate(valid => {
         if (valid) {
+          // 格式化时间字段
+          this.form.executeTime = this.formatDateTime(this.form.executeTime);
+          this.form.plannedCompletetime = this.formatDateTime(this.form.plannedCompletetime);
+
           if (this.form.id != null) {
-            this.form.field = this.form.plotId
+            // 修改任务
+            this.form.field = this.form.plotId;
             const selectedUser = this.userName.find(user => user.userId === this.form.assigneeId);
             if (selectedUser) {
               this.form.assigneeName = selectedUser.nickName;
@@ -579,14 +644,15 @@ export default {
             if (selectedfieldName) {
               this.form.fieldName = selectedfieldName.fieldName;
             }
-            this.form.executeTime = this.formatDateTime(this.form.executeTime);
+
             updateTasks(this.form).then(response => {
-              this.$modal.msgSuccess("修改成功")
-              this.open = false
-              this.getList()
-            })
+              this.$modal.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
           } else {
-            this.form.field = this.form.plotId
+            // 新增任务
+            this.form.field = this.form.plotId;
             const selectedUser = this.userName.find(user => user.userId === this.form.assigneeId);
             if (selectedUser) {
               this.form.assigneeName = selectedUser.nickName;
@@ -596,15 +662,15 @@ export default {
             if (selectedfieldName) {
               this.form.fieldName = selectedfieldName.fieldName;
             }
-            this.form.executeTime = this.formatDateTime(this.form.executeTime);
+
             addTasks(this.form).then(response => {
-              this.$modal.msgSuccess("新增成功")
-              this.open = false
-              this.getList()
-            })
+              this.$modal.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
           }
         }
-      })
+      });
     },
     /** 删除按钮操作 */
     handleDelete(row) {

+ 238 - 19
src/views/base/tasks/stats/index.vue

@@ -68,10 +68,15 @@
           </el-select>
         </el-form-item>
 
-        <el-form-item label="任务类型" prop="taskTypes">
+        <!-- <el-form-item label="任务类型" prop="taskTypes">
           <el-select v-model="filters.taskTypes" placeholder="请选择任务类型" multiple collapse-tags style="width: 180px">
             <el-option v-for="item in taskTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
           </el-select>
+        </el-form-item> -->
+        <el-form-item label="任务类型" prop="typeName">
+          <el-select v-model="filters.typeName" placeholder="请选择任务类型" clearable>
+            <el-option v-for="dict in dict.type.task_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+          </el-select>
         </el-form-item>
 
         <el-form-item label="执行人" prop="executors">
@@ -80,10 +85,16 @@
           </el-select>
         </el-form-item>
 
-        <el-form-item label="状态" prop="statuses">
+        <!-- <el-form-item label="状态" prop="statuses">
           <el-select v-model="filters.statuses" placeholder="请选择状态" multiple collapse-tags style="width: 180px">
             <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
           </el-select>
+        </el-form-item> -->
+        <el-form-item label="任务状态" prop="taskStatus">
+          <el-select v-model="filters.taskStatus" placeholder="请选择任务状态" clearable>
+            <el-option v-for="dict in dict.type.task_status" :key="dict.value" :label="dict.label"
+              :value="dict.value" />
+          </el-select>
         </el-form-item>
 
         <el-form-item>
@@ -102,7 +113,7 @@
     <!-- KPI 卡片区域 -->
     <div class="kpi-section stats-card">
       <div class="kpi-grid">
-        <div v-for="(kpi, index) in kpiData" :key="index" class="kpi-card" @click="handleKpiClick(kpi.key)">
+        <div v-for="(kpi, index) in kpiData" :key="index" class="kpi-card">
           <div class="kpi-card__icon" :style="{ backgroundColor: kpi.iconBg }">
             <i :class="kpi.icon" :style="{ color: kpi.color }"></i>
           </div>
@@ -117,7 +128,7 @@
               <span v-if="kpi.unit" class="kpi-unit">{{ kpi.unit }}</span>
             </div>
           </div>
-          <el-dropdown trigger="click" @command="handleKpiAction" class="kpi-card__menu">
+          <!-- <el-dropdown trigger="click" @command="handleKpiAction" class="kpi-card__menu">
             <span class="kpi-menu-trigger">
               <i class="el-icon-more"></i>
             </span>
@@ -129,7 +140,7 @@
                 <i class="el-icon-view"></i> 查看明细
               </el-dropdown-item>
             </el-dropdown-menu>
-          </el-dropdown>
+          </el-dropdown> -->
         </div>
       </div>
     </div>
@@ -365,6 +376,7 @@ import seedrandom from 'seedrandom'
 import CountTo from 'vue-count-to'
 import { deptTreeSelect } from "@/api/system/user";
 import { listFieldName } from "@/api/base/field"
+import { listTasksStatistics } from "@/api/base/tasks"
 import Treeselect from "@riophae/vue-treeselect";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
 
@@ -374,6 +386,7 @@ export default {
     CountTo,
     Treeselect
   },
+  dicts: ['task_type', 'task_status', 'field_soil_type', 'sys_field_type'],
   data() {
     return {
       // 部门树选项
@@ -427,9 +440,9 @@ export default {
         farms: [],
         plots: [],
         crops: [],
-        taskTypes: [],
+        typeName: [],
         executors: [],
-        statuses: []
+        taskStatus: []
       },
 
       // 下拉选项
@@ -525,6 +538,13 @@ export default {
     this.getDeptTree();
     this.getFieldNameList();
     // this.initOptions()
+    /* const queryParams = {
+
+    }
+    listTasksStatistics(queryParams).then(response => {
+        console.log(response)
+        console.log(response.data)
+      }) */
   },
 
   mounted() {
@@ -603,9 +623,9 @@ export default {
     // 加载所有数据
     loadAllData() {
       this.loadKpiData()
-      this.loadTrendData()
-      this.loadDimensionData()
-      this.loadTodoData()
+      this.loadTrendData() // 加载趋势数据
+      this.loadDimensionData() // 加载维度对比数据
+      this.loadTodoData() // 加载待办数据
     },
 
     // 初始化图表
@@ -639,6 +659,7 @@ export default {
         this.formatDate(start),
         this.formatDate(end)
       ]
+      console.log(this.filters.dateRange);
       this.loadAllData()
     },
 
@@ -701,8 +722,87 @@ export default {
 
     // 加载KPI数据
     loadKpiData() {
-      const kpi = this.mockKpi(this.filters)
-      this.kpiData = [
+      console.log(this.filters)
+      /* const kpi = this.mockKpi(this.filters) */
+      this.filters.params = {}
+      if (null != this.filters.dateRange && '' != this.filters.dateRange) {
+        this.filters.params["beginCreateTime"] = this.filters.dateRange[0];
+        this.filters.params["endCreateTime"] = this.filters.dateRange[1];
+      }
+      listTasksStatistics(this.filters).then(response => {
+        console.log("加载KPI数据")
+        console.log(response)
+        console.log(response.data)
+        const kpi = response.data
+
+        this.kpiData = [
+          {
+            title: '任务总数',
+            value: kpi.taskCount,
+            type: 'number',
+            color: '#1F2937',
+            key: 'total',
+            icon: 'el-icon-s-data',
+            iconBg: 'rgba(74, 144, 226, 0.12)'
+          },
+          {
+            title: '已完成',
+            value: kpi.completedCount,
+            type: 'number',
+            color: '#3BB44A',
+            key: 'completed',
+            icon: 'el-icon-circle-check',
+            iconBg: 'rgba(59, 180, 74, 0.12)'
+          },
+          {
+            title: '待完成',
+            value: kpi.pendingCount,
+            type: 'number',
+            color: '#3BB44A',
+            key: 'completion_rate',
+            icon: 'el-icon-pie-chart',
+            iconBg: 'rgba(59, 180, 74, 0.12)'
+          },
+          {
+            title: '逾期数',
+            value: kpi.overdueCount,
+            type: 'number',
+            color: '#4A90E2',
+            key: 'ontime_rate',
+            icon: 'el-icon-timer',
+            iconBg: 'rgba(74, 144, 226, 0.12)'
+          },
+          {
+            title: '完成率',
+            value: kpi.completionRate * 100,
+            type: 'percent',
+            color: '#20B2AA',
+            key: 'avg_duration',
+            icon: 'el-icon-stopwatch',
+            iconBg: 'rgba(32, 178, 170, 0.12)',
+            unit: kpi.durationUnit
+          },
+          {
+            title: '准时率',
+            value: kpi.onTimeRate * 100,
+            type: 'percent',
+            color: '#E85D75',
+            key: 'overdue',
+            icon: 'el-icon-warning',
+            iconBg: 'rgba(232, 93, 117, 0.12)'
+          },
+          {
+            title: '平均完成时长',
+            value: kpi.avgCompletionDuration,
+            type: 'number',
+            color: '#4A90E2',
+            key: 'in_progress',
+            icon: 'el-icon-loading',
+            iconBg: 'rgba(74, 144, 226, 0.12)'
+          }
+        ]
+      })
+      /* this.kpiData = [
         {
           title: '任务总数',
           value: kpi.total,
@@ -776,18 +876,137 @@ export default {
           icon: 'el-icon-time',
           iconBg: 'rgba(139, 148, 158, 0.12)'
         }
-      ]
+      ] */
+
     },
 
     // 加载趋势数据
     loadTrendData() {
       this.trendLoading = true
       setTimeout(() => {
-      console.log("进入mockTrend的条件")
-      console.log(this.filters)
-      console.log(this.trendMetric)
-        const data = this.mockTrend(this.filters, this.trendMetric)
-        console.log("loadTrendData加载趋势数据")
+        const data = [
+          {
+            "date": "2025-10-28",
+            "value": 50
+          },
+          {
+            "date": "2025-10-29",
+            "value": 45
+          },
+          {
+            "date": "2025-10-30",
+            "value": 0
+          },
+          {
+            "date": "2025-10-31",
+            "value": 0
+          },
+          {
+            "date": "2025-11-01",
+            "value": 0
+          },
+          {
+            "date": "2025-11-02",
+            "value": 0
+          },
+          {
+            "date": "2025-11-03",
+            "value": 0
+          },
+          {
+            "date": "2025-11-04",
+            "value": 47
+          },
+          {
+            "date": "2025-11-05",
+            "value": 32
+          },
+          {
+            "date": "2025-11-06",
+            "value": 36
+          },
+          {
+            "date": "2025-11-07",
+            "value": 40
+          },
+          {
+            "date": "2025-11-08",
+            "value": 10
+          },
+          {
+            "date": "2025-11-09",
+            "value": 34
+          },
+          {
+            "date": "2025-11-10",
+            "value": 52
+          },
+          {
+            "date": "2025-11-11",
+            "value": 18
+          },
+          {
+            "date": "2025-11-12",
+            "value": 23
+          },
+          {
+            "date": "2025-11-13",
+            "value": 36
+          },
+          {
+            "date": "2025-11-14",
+            "value": 29
+          },
+          {
+            "date": "2025-11-15",
+            "value": 19
+          },
+          {
+            "date": "2025-11-16",
+            "value": 26
+          },
+          {
+            "date": "2025-11-17",
+            "value": 24
+          },
+          {
+            "date": "2025-11-18",
+            "value": 47
+          },
+          {
+            "date": "2025-11-19",
+            "value": 44
+          },
+          {
+            "date": "2025-11-20",
+            "value": 11
+          },
+          {
+            "date": "2025-11-21",
+            "value": 52
+          },
+          {
+            "date": "2025-11-22",
+            "value": 10
+          },
+          {
+            "date": "2025-11-23",
+            "value": 48
+          },
+          {
+            "date": "2025-11-24",
+            "value": 39
+          },
+          {
+            "date": "2025-11-25",
+            "value": 12
+          },
+          {
+            "date": "2025-11-26",
+            "value": 53
+          }
+        ]
+        console.log("加载趋势数据")
         console.log(data)
         this.updateTrendChart(data)
         this.trendLoading = false
@@ -1379,7 +1598,7 @@ export default {
       const random = seedrandom(seed)
 
       const data = []
-      const days = 60
+      const days = 30
       const startDate = new Date(filters.dateRange[0])
 
       for (let i = 0; i < days; i++) {

+ 2 - 2
src/views/login.vue

@@ -73,8 +73,8 @@ export default {
       title: process.env.VUE_APP_TITLE,
       codeUrl: "",
       loginForm: {
-        username: "admin",
-        password: "gbd2025",
+        username: "",
+        password: "",
         rememberMe: false,
         code: "",
         uuid: ""

部分文件因文件數量過多而無法顯示