Просмотр исходного кода

feat:添加勾画地块页面,修改上传组件

wangsisi 1 неделя назад
Родитель
Сommit
db19990a80

+ 12 - 7
src/components/upload.vue

@@ -32,7 +32,9 @@
   <popup v-model:show="showExamplePopup" overlay-class="example-overlay" class="example-popup">
     <div class="example-content">
       <!-- <img src="@/assets/img/home/example-4.png" alt="" /> -->
-      <img class="example-img" :src="exampleImgData || 'https://birdseye-img-ali-cdn.sysuimars.com/birdseye-look-mini/94379/1768801082504.png'" alt="" />
+      <img class="example-img"
+        :src="exampleImgData || 'https://birdseye-img-ali-cdn.sysuimars.com/birdseye-look-mini/94379/1768801082504.png'"
+        alt="" />
       <div class="example-tips">
         拍摄要求:请采集代表农场作物物候期的照片,请采集代表农场作物物候期的照片。
       </div>
@@ -173,7 +175,7 @@ onMounted(() => {
 
       .image-item-wrapper {
         position: relative;
-        margin-right: 8px;
+        margin-right: 6px;
 
         &::after {
           content: '示例';
@@ -247,13 +249,18 @@ onMounted(() => {
   }
 
   .uploader {
-
     .plus,
-    .example,
-    .van-uploader__wrapper {
+    .example {
       width: calc((100vw - 68px) / 4);
       height: calc((100vw - 68px) / 4);
     }
+
+    ::v-deep {
+      .van-uploader__wrapper {
+        --van-uploader-size: 76.7px;
+        --van-padding-xs: 6px;
+      }
+    }
   }
 
   .uploader-list {
@@ -282,8 +289,6 @@ onMounted(() => {
   }
 }
 
-
-
 .example-popup {
   width: 100%;
   border-radius: 0;

+ 6 - 0
src/router/globalRoutes.js

@@ -439,4 +439,10 @@ export default [
         name: "InteractionList",
         component: () => import("@/views/old_mini/interactionList/index.vue"),
     },
+    // 勾画发生区域
+    {
+        path: "/draw_region",
+        name: "DrawRegion",
+        component: () => import("@/views/old_mini/interactionList/drawRegion.vue"),
+    },
 ];

+ 166 - 0
src/views/old_mini/interactionList/drawRegion.vue

@@ -0,0 +1,166 @@
+<template>
+    <div class="edit-map">
+        <custom-header name="勾画区域"></custom-header>
+        <div class="edit-map-content">
+            <div class="edit-map-tip">操作提示:拖动圆点,即可调整地块边界</div>
+            <div class="map-container" ref="mapContainer"></div>
+            <div class="edit-map-footer">
+                <div class="footer-back" @click="goBack">
+                    <img class="back-icon" src="@/assets/img/home/go-back.png" alt="" />
+                </div>
+                <div class="edit-map-footer-btn">
+                    <div class="btn-delete" @click="deletePolygon">删除地块</div>
+                    <div class="btn-cancel" @click="goBack">取消</div>
+                    <div class="btn-confirm" @click="confirm">确认</div>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import customHeader from "@/components/customHeader.vue";
+import { ref, onMounted, onActivated, onDeactivated } from "vue";
+import DrawRegionMap from "./map/drawRegionMap.js";
+import { useRouter, useRoute } from "vue-router";
+import { convertPointToArray } from "@/utils/index";
+import { useStore } from "vuex";
+import { ElMessage, ElMessageBox } from "element-plus";
+
+const router = useRouter();
+const route = useRoute();
+const store = useStore();
+const mapContainer = ref(null);
+const drawRegionMap = new DrawRegionMap();
+
+const type = ref(null)
+onMounted(() => {
+    type.value = route.query.type
+    const point = route.query.mapCenter || "POINT (113.6142086995688 23.585836479509055)"
+    drawRegionMap.initMap(point, mapContainer.value);
+    
+    // 设置绘制限制回调
+    drawRegionMap.setDrawLimitCallback(() => {
+        ElMessage.warning("请先删除当前地块再重新勾画");
+    });
+});
+
+onActivated(() => {
+    const point = route.query.mapCenter || "POINT (113.6142086995688 23.585836479509055)"
+    
+    // 先绘制地块
+    const polygonData = store.state.home.polygonData;
+    if (polygonData) {
+        drawRegionMap.setAreaGeometry(polygonData?.geometryArr);
+    }
+    
+    // 再设置地图中心位置,确保视图在 mapCenter
+    drawRegionMap.setMapPosition(convertPointToArray(point))
+})
+
+onDeactivated(() => {
+    drawRegionMap.clearLayer()
+})
+const goBack = () => {
+    // drawRegionMap.clearLayer()
+    router.back()
+};
+
+const deletePolygon = () => {
+    ElMessageBox.confirm(
+        '确认要删除当前地块吗?删除后可以重新勾画。',
+        '删除确认',
+        {
+            confirmButtonText: '确认删除',
+            cancelButtonText: '取消',
+            type: 'warning',
+        }
+    ).then(() => {
+        drawRegionMap.deleteCurrentPolygon();
+        ElMessage.success("地块已删除");
+    }).catch(() => {
+        // 用户取消删除,不做任何操作
+    });
+};
+
+const confirm = () => {
+    // getAreaGeometry
+    const polygonData = drawRegionMap.getAreaGeometry()
+    console.log("polygonData", polygonData);
+    sessionStorage.setItem("drawRegionPolygonData", JSON.stringify(polygonData));
+    router.back()
+};
+</script>
+
+<style lang="scss" scoped>
+.edit-map {
+    width: 100%;
+    height: 100vh;
+    overflow: hidden;
+    .edit-map-content {
+        width: 100%;
+        height: 100%;
+        position: relative;
+        .edit-map-tip {
+            position: absolute;
+            top: 23px;
+            left: calc(50% - 256px / 2);
+            z-index: 1;
+            font-size: 12px;
+            color: #fff;
+            padding: 9px 20px;
+            background: rgba(0, 0, 0, 0.5);
+            border-radius: 20px;
+        }
+        .map-container {
+            width: 100%;
+            height: 100%;
+        }
+        .edit-map-footer {
+            position: absolute;
+            bottom: 80px;
+            left: 12px;
+            width: calc(100% - 24px);
+            display: flex;
+            flex-direction: column;
+            align-items: flex-end;
+            .footer-back {
+                padding: 6px 7px 9px;
+                background: #fff;
+                border-radius: 8px;
+                margin-bottom: 12px;
+                .back-icon {
+                    width: 20px;
+                    height: 18px;
+                }
+            }
+            .edit-map-footer-btn {
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                width: 100%;
+                gap: 8px;
+                div {
+                    flex: 1;
+                    max-width: 100px;
+                    text-align: center;
+                    color: #666666;
+                    font-size: 14px;
+                    padding: 8px 0;
+                    border-radius: 25px;
+                    background: #fff;
+                }
+                .btn-delete {
+                    background: #ff4d4f;
+                    color: #fff;
+                }
+                .btn-confirm {
+                    background: #000;
+                    background-image: linear-gradient(180deg, #76c3ff 0%, #2199f8 100%);
+                    color: #fff;
+                }
+            }
+        }
+    }
+}
+</style>

+ 94 - 73
src/views/old_mini/interactionList/index.vue

@@ -2,69 +2,69 @@
     <custom-header name="农情互动" bgColor="#f2f4f5"></custom-header>
     <div class="interaction-list">
         <div class="list-item" v-for="(item, index) in listData" :key="item.id || index"
-            :class="{ 'uploaded-item': item.imageIds.length }">
-                <!-- 标题区域 -->
-                <div class="item-header-wrapper" :class="{ 'has-status': item.imageIds.length }">
-                    <div class="item-header">{{ item.interactionTypeName }}</div>
-                    <div class="upload-status" v-show="item.imageIds.length">
-                        <el-icon class="status-icon">
-                            <SuccessFilled />
-                        </el-icon>
-                        <span class="status-text">上传成功</span>
-                    </div>
+            :class="{ 'uploaded-item': item.isConfirmed != null }">
+            <!-- 标题区域 -->
+            <div class="item-header-wrapper" :class="{ 'has-status': item.isConfirmed != null }">
+                <div class="item-header">{{ item.interactionTypeName }}</div>
+                <div class="upload-status" v-show="item.isConfirmed != null">
+                    <el-icon class="status-icon">
+                        <SuccessFilled />
+                    </el-icon>
+                    <span class="status-text">上传成功</span>
                 </div>
+            </div>
 
-                <!-- 展开状态内容 -->
-                <div class="expanded-content" v-show="item.imageIds.length && item.expanded">
-                    <!-- 原因说明 -->
-                    <div class="reason-text">原因原因: 便于记录农场档案,及时调整农事规划</div>
+            <!-- 展开状态内容 -->
+            <div class="expanded-content" v-show="item.isConfirmed != null && item.expanded">
+                <!-- 原因说明 -->
+                <div class="reason-text">原因: {{ item.reason }}</div>
 
-                    <!-- 图片展示 -->
-                    <div class="uploaded-images">
-                        <img class="uploaded-img" src="@/assets/img/home/example-4.png" alt="" />
-                        <img class="uploaded-img" src="@/assets/img/home/example-4.png" alt="" />
-                        <img class="uploaded-img" src="@/assets/img/home/example-4.png" alt="" />
-                    </div>
+                <!-- 图片展示 -->
+                <div class="uploaded-images" v-show="item.imagePaths.length > 0">
+                    <img class="uploaded-img" v-for="image in item.imagePaths" :key="image" :src="base_img_url2 + image" alt="" />
                 </div>
+            </div>
 
-                <!-- 未上传状态内容 -->
-                <div v-show="!item.imageIds.length">
-                    <!-- 说明文字 -->
-                    <div class="item-desc">{{ item.question }}</div>
+            <!-- 未上传状态内容 -->
+            <div v-show="item.isConfirmed == null">
+                <!-- 说明文字 -->
+                <div class="item-desc">{{ item.question }}</div>
 
-                    <upload @handleUpload="handleUploadSuccess" :exampleList="item.exampleImagesJson"></upload>
+                <upload :maxCount="8" @handleUpload="handleUploadSuccess" :exampleList="item.exampleImagesJson">
+                </upload>
 
-                    <div class="question-wrapper">
-                        <div class="question-text">
-                            <span class="text-title">{{ item.phenologyName }}占比</span>
-                            <el-input v-model="item.replyText" type="number" style="width: 80px" />
-                            <span class="text-unit">%</span>
-                        </div>
-                        <div class="draw-region-btn" @click="handleDrawRegion(item)">勾画发生区域</div>
-                    </div>
-                    <!-- 输入框 -->
-                    <div class="input-wrapper">
-                        <el-input v-model="item.remark" placeholder="添加备注:" clearable />
+                <div class="question-wrapper">
+                    <div class="question-text">
+                        <span class="text-title">{{ item.phenologyName }}占比</span>
+                        <el-input v-model="item.replyText" type="number" style="width: 80px" />
+                        <span class="text-unit">%</span>
                     </div>
+                    <div class="draw-region-btn" @click="handleDrawRegion(item)">勾画发生区域</div>
+                </div>
+                <!-- 输入框 -->
+                <div class="input-wrapper">
+                    <el-input v-model="item.remark" placeholder="添加备注:" clearable />
+                </div>
 
-                    <!-- 按钮区域 -->
-                    <div class="button-group">
-                        <div class="btn-not-reached" @click="handleNotReached(item)">{{ item.cancelButtonName }}</div>
-                        <div class="btn-confirm" @click="handleConfirm(item)">确认上传</div>
-                    </div>
+                <!-- 按钮区域 -->
+                <div class="button-group">
+                    <div class="btn-not-reached" @click="handleNotReached(item)">{{ item.cancelButtonName }}</div>
+                    <div class="btn-confirm" @click="handleConfirm(item)">确认上传</div>
                 </div>
+            </div>
 
-                <!-- 比例信息(已上传状态显示) -->
-                <div class="proportion-info" v-show="item.imageIds.length">
-                    <span class="proportion-text">当前果园达到物候期的比例: {{ item.proportion || "10%" }}</span>
-                    <div class="toggle-btn" @click="toggleExpand(item)">
-                        <span>{{ item.expanded ? "收起" : "展开" }}</span>
-                        <el-icon :class="{ rotate: !item.expanded }">
-                            <CaretTop />
-                        </el-icon>
-                    </div>
+            <!-- 比例信息(已上传状态显示) -->
+            <div class="proportion-info" v-show="item.isConfirmed != null">
+                <span class="proportion-text" v-if="item.replyText">当前果园{{ item.phenologyName }}占比: {{ item.replyText }}%</span>
+                <span class="proportion-text" v-else>暂无数据</span>
+                <div class="toggle-btn" @click="toggleExpand(item)">
+                    <span>{{ item.expanded ? "收起" : "展开" }}</span>
+                    <el-icon :class="{ rotate: !item.expanded }">
+                        <CaretTop />
+                    </el-icon>
                 </div>
             </div>
+        </div>
 
         <div class="empty-data" v-if="!loading && listData.length === 0">暂无数据</div>
     </div>
@@ -79,6 +79,7 @@ import customHeader from "@/components/customHeader.vue";
 import upload from "@/components/upload.vue";
 import FarmInfoPopup from "@/components/popup/farmInfoPopup.vue";
 import { useRouter, useRoute } from "vue-router";
+import { base_img_url2 } from "@/api/config";
 const showFarmInfoPopup = ref(false);
 const loading = ref(false);
 const router = useRouter();
@@ -94,8 +95,8 @@ const loadData = async () => {
             // 将 exampleImagesJson 转换为数组
             if (item.exampleImagesJson) {
                 try {
-                    item.exampleImagesJson = typeof item.exampleImagesJson === 'string' 
-                        ? JSON.parse(item.exampleImagesJson) 
+                    item.exampleImagesJson = typeof item.exampleImagesJson === 'string'
+                        ? JSON.parse(item.exampleImagesJson)
                         : item.exampleImagesJson;
                     // 确保是数组格式
                     if (!Array.isArray(item.exampleImagesJson)) {
@@ -125,17 +126,16 @@ const refreshList = async () => {
 
 // 暂未到达进程
 const handleNotReached = async (item) => {
-    // 这里可以处理业务逻辑
     const parmas = {
-        isConfirmed:false,
+        isConfirmed: false,
         farmId: localStorage.getItem("selectedFarmId"),
         imagePaths: uploadData.value,
         isUploadPhoto: uploadData.value.length > 0 ? true : false,
-        rangeWkt:'',
-        interactionId:item.id,
-        replyText:item.replyText
+        rangeWkt: '',
+        interactionId: item.id,
+        replyText: item.replyText
     }
-    const {code, msg} = await VE_API.home.uploadAnswer(parmas);
+    const { code, msg } = await VE_API.home.uploadAnswer(parmas);
     if (code === 0) {
         ElMessage.success("回答成功");
         // 清空上传数据
@@ -149,7 +149,7 @@ const handleNotReached = async (item) => {
 
 // 确认上传
 const handleConfirm = async (item) => {
-    if (!item.proportion) {
+    if (!item.phenologyName) {
         ElMessage.warning("请输入当前果园比例");
         return;
     }
@@ -157,15 +157,26 @@ const handleConfirm = async (item) => {
         ElMessage.warning("请上传图片");
         return;
     }
-    console.log("确认上传", item);
-    item.expanded = false;
-    // 这里可以处理提交逻辑
-    // const {code, msg} = await VE_API.home.uploadAnswer(item);
-    // if (code === 0) {
-    //     ElMessage.success("上传成功");
-    // } else {
-    //     ElMessage.error(msg || '上传失败');
-    // }
+    const parmas = {
+        isConfirmed: true,
+        farmId: localStorage.getItem("selectedFarmId"),
+        imagePaths: uploadData.value,
+        isUploadPhoto: uploadData.value.length > 0 ? true : false,
+        rangeWkt: sessionStorage.getItem("drawRegionPolygonData"),
+        interactionId: item.id,
+        replyText: item.replyText
+    }
+    const { code, msg } = await VE_API.home.uploadAnswer(parmas);
+    if (code === 0) {
+        ElMessage.success("上传成功");
+        sessionStorage.removeItem("drawRegionPolygonData");
+        // 清空上传数据
+        uploadData.value = [];
+        // 刷新列表
+        await refreshList();
+    } else {
+        ElMessage.error(msg || '上传失败');
+    }
 };
 
 // 切换展开/收起
@@ -186,6 +197,8 @@ const handleFarmInfoConfirm = async (data) => {
 
 const handleDrawRegion = (item) => {
     console.log("勾画发生区域", item);
+    router.push(`/draw_region`)
+    // router.push('/edit_map?type=edit')
 };
 
 const uploadData = ref([]);
@@ -222,6 +235,10 @@ const getFarmList = async () => {
         background: #ffffff;
         border-radius: 6px;
         padding: 10px;
+        border: 1px solid #ffffff;
+        &.uploaded-item{
+            border: 1px solid #2199f8;
+        }
 
         .item-header-wrapper {
             .item-header {
@@ -265,19 +282,20 @@ const getFarmList = async () => {
 
         .expanded-content {
             .reason-text {
-                padding: 12px 0;
+                padding-top: 12px;
                 color: #969696;
                 font-size: 14px;
             }
 
             .uploaded-images {
                 display: flex;
-                gap: 12px;
-                margin-bottom: 12px;
+                flex-wrap: wrap;
+                gap: 8px;
+                margin: 12px 0;
 
                 .uploaded-img {
-                    width: 80px;
-                    height: 80px;
+                    width: calc((100vw - 68px) / 4);
+                    height: calc((100vw - 68px) / 4);
                     border-radius: 4px;
                     object-fit: cover;
                 }
@@ -358,6 +376,7 @@ const getFarmList = async () => {
         color: #999999;
         font-size: 14px;
     }
+
     .question-wrapper {
         display: flex;
         align-items: center;
@@ -372,9 +391,11 @@ const getFarmList = async () => {
             color: #6f6f6f;
             display: flex;
             align-items: center;
+
             .text-title {
                 margin-right: 4px;
             }
+
             .text-unit {
                 margin-left: 4px;
             }

+ 146 - 0
src/views/old_mini/interactionList/map/drawRegionMap.js

@@ -0,0 +1,146 @@
+import * as KMap from "@/utils/ol-map/KMap";
+import * as util from "@/common/ol_common.js";
+import config from "@/api/config.js";
+import Style from "ol/style/Style";
+import Icon from "ol/style/Icon";
+import { Point } from 'ol/geom';
+import Feature from "ol/Feature";
+import * as proj from "ol/proj";
+import { getArea } from 'ol/sphere.js';
+import WKT from "ol/format/WKT.js";
+import proj4 from "proj4"
+import { register } from "ol/proj/proj4";
+proj4.defs("EPSG:38572", "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs");
+register(proj4);
+
+/**
+ * @description 地图层对象
+ */
+class DrawRegionMap {
+    constructor() {
+        let that = this;
+        let vectorStyle = new KMap.VectorStyle();
+        this.vectorStyle = vectorStyle;
+
+        // 位置图标
+        this.clickPointLayer = new KMap.VectorLayer("clickPointLayer", 9999, {
+            style: (f) => {
+                return new Style({
+                    image: new Icon({
+                        src: require("@/assets/img/home/garden-point.png"),
+                        scale: 0.5,
+                        anchor: [0.5, 1],
+                    }),
+                });
+            },
+        });
+    }
+
+    initMap(location, target) {
+        let level = 16;
+        let coordinate = util.wktCastGeom(location).getFirstCoordinate();
+        this.kmap = new KMap.Map(target, level, coordinate[0], coordinate[1], null, 8, 22);
+        let xyz2 = config.base_img_url3 + "map/lby/{z}/{x}/{y}.png";
+        this.kmap.addXYZLayer(xyz2, { minZoom: 8, maxZoom: 22 }, 2);
+        this.kmap.addLayer(this.clickPointLayer.layer);
+        this.setMapPoint(coordinate)
+
+        this.kmap.initDraw((e) => {
+            // drawend事件:绘制结束后的处理
+        })
+
+        // 监听drawstart事件,在开始绘制前检查是否已有地块
+        this.kmap.draw.on('drawstart', (e) => {
+            const features = this.kmap.getLayerFeatures();
+            if (features && features.length >= 1) {
+                // 提示用户先删除当前地块
+                this.onDrawLimit && this.onDrawLimit();
+                // 取消本次绘制
+                this.kmap.draw.abortDrawing();
+            }
+        });
+
+        this.kmap.startDraw()
+        this.kmap.modifyDraw()
+    }
+
+    setAreaGeometry(geometryArr, needFitView = false) {
+        let that = this
+        geometryArr.map(item => {
+            // 不使用 setLayerWkt,而是手动添加要素,避免自动缩放视图
+            const format = new WKT()
+            const mapProjection = that.kmap.map.getView().getProjection()
+            let geometry = format.readGeometry(item, {
+                dataProjection: 'EPSG:4326',
+                featureProjection: mapProjection
+            })
+            let f = new Feature({ geometry: geometry })
+            that.kmap.polygonLayer.source.addFeature(f)
+        })
+        // 根据参数决定是否需要自适应地块范围
+        if (needFitView) {
+            this.fitView()
+        }
+    }
+
+
+    fitView() {
+        let extent = this.kmap.polygonLayer.source.getExtent()
+        // 地图自适应到区域可视范围
+        this.kmap.getView().fit(extent, { duration: 500, padding: [100, 100, 100, 100] });
+    }
+
+    clearLayer() {
+        // this.kmap.removeLayer(this.clickPointLayer.layer)
+        this.kmap.polygonLayer.source.clear();
+    }
+
+    getAreaGeometry() {
+        const features = this.kmap.getLayerFeatures()
+        let geometryArr = []
+        let area = 0
+        const format = new WKT()
+        // 获取图层上的Polygon,转成WKT用于回显
+        features.forEach(item => {
+            // 使用 writeGeometry 而不是 writeFeature,因为 setLayerWkt 期望的是几何体的 WKT
+            const geometry = item.getGeometry()
+            geometryArr.push(format.writeGeometry(geometry, {
+                dataProjection: 'EPSG:4326',
+                featureProjection: this.kmap.map.getView().getProjection()
+            }))
+            let geom = geometry.clone()
+            geom.transform(proj.get("EPSG:4326"), proj.get("EPSG:38572"))
+            let areaItem = getArea(geom)
+            areaItem = (areaItem + areaItem / 2) / 1000;
+            area += areaItem
+        })
+        return { geometryArr, mianji: area.toFixed(2) }  // 修改为 mianji 字段,与创建页面保持一致
+    }
+
+    setMapPosition(center) {
+        this.kmap.getView().animate({
+            center,
+            zoom: 17,
+            duration: 500,
+        });
+        this.setMapPoint(center)
+    }
+
+    setMapPoint(coordinate) {
+        this.clickPointLayer.source.clear()
+        let point = new Feature(new Point(coordinate))
+        this.clickPointLayer.addFeature(point)
+    }
+
+    // 删除当前地块
+    deleteCurrentPolygon() {
+        this.kmap.polygonLayer.source.clear();
+    }
+
+    // 设置绘制限制回调
+    setDrawLimitCallback(callback) {
+        this.onDrawLimit = callback;
+    }
+}
+
+export default DrawRegionMap;