2 Achegas 0d62522205 ... d3cd2fdf90

Autor SHA1 Mensaxe Data
  wangsisi d3cd2fdf90 Merge branch 'agriculture' of http://www.sysuimars.cn:3000/feiniao/feiniao-farm-h5 into agriculture hai 5 días
  wangsisi d9452e457b feat:添加农事详情 hai 5 días

+ 7 - 0
src/router/globalRoutes.js

@@ -135,6 +135,13 @@ export default [
         meta: { keepAlive: true },
         component: () => import("@/views/old_mini/recordDetails/mapManage.vue"),
     },
+    // 农事详情
+    {
+        path: "/agri_record_detail",
+        name: "AgriRecordDetail",
+        meta: { keepAlive: true },
+        component: () => import("@/views/old_mini/agri_record_detail/index.vue"),
+    },
     // 农事执行
     {
         path: "/work_execute",

+ 51 - 13
src/views/old_mini/agri_record/index.vue

@@ -3,10 +3,13 @@
     <div class="monitor-index" :style="{ height: `calc(100vh - ${tabBarHeight}px)` }">
         <!-- 天气遮罩 -->
         <div class="weather-mask" v-show="isExpanded" @click="handleMaskClick"></div>
-        <!-- 天气 -->
-        <weather-info ref="weatherInfoRef" from="agri_record" class="weather-info" @weatherExpanded="weatherExpanded"
-            @changeGarden="changeGarden" @changeGardenTab="changeGardenTab" :isGarden="true"
-            :gardenId="defaultGardenId"></weather-info>
+        <!-- 头部 -->
+        <div v-show="activeGardenTab === 'current'" class="agri-record-header">
+            <weather-info ref="weatherInfoRef" :hasWeather="false" from="agri_record" class="weather-info"
+                @weatherExpanded="weatherExpanded" @changeGarden="changeGarden" @changeGardenTab="changeGardenTab"
+                @reportTabClick="handleReportTabClick" @farmInfoMaintain="handleFarmInfoMaintain" :isGarden="true"
+                :gardenId="defaultGardenId" />
+        </div>
         <!-- 农场列表 -->
         <div v-show="activeGardenTab === 'list'">
             <garden-list ref="gardenListRef" :garden-id="selectedGardenId" @loaded="handleGardenLoaded"
@@ -42,11 +45,12 @@ import customHeader from "@/components/customHeader.vue";
 import { ref, computed, onActivated, onDeactivated, watch, nextTick } from "vue";
 import { useStore } from "vuex";
 import weatherInfo from "@/components/weatherInfo.vue";
-import { useRoute, onBeforeRouteLeave } from "vue-router";
+import { useRoute, useRouter, onBeforeRouteLeave } from "vue-router";
 import ArchivesFarmTimeLine from "@/components/pageComponents/ArchivesFarmTimeLine.vue";
 import gardenList from "@/components/gardenList.vue";
 
 const route = useRoute();
+const router = useRouter();
 
 const archivesScrollAreaRef = ref(null);
 const getArchivesOuterScrollKey = () =>
@@ -94,6 +98,21 @@ const changeGardenTab = (tab) => {
     activeGardenTab.value = tab;
 };
 
+const currentFarmName = ref("");
+const currentFarmVariety = ref(null);
+
+const handleReportTabClick = (item) => {
+    if (item.key === "historyRisk") {
+        router.push(
+            `/history_risk_report?farmVariety=${currentFarmVariety.value ?? ""}&currentFarmName=${currentFarmName.value ?? ""}`
+        );
+    }
+};
+
+const handleFarmInfoMaintain = (farmId) => {
+    router.push(`/create_farm?type=edit&farmId=${farmId}&from=agri_record`);
+};
+
 const handleGardenLoaded = ({ hasFarm }) => {
     weatherInfoRef.value?.setGardenLoaded?.(hasFarm);
 };
@@ -160,9 +179,13 @@ const handleMaskClick = () => {
 
 const gardenId = ref(store.state.home.gardenId);
 
-const changeGarden = ({ id }) => {
-    gardenId.value = id;
-    store.commit("home/SET_GARDEN_ID", id);
+const changeGarden = (data) => {
+    if (!data?.id) return;
+    gardenId.value = data.id;
+    selectedGardenId.value = data.id;
+    currentFarmName.value = data.name ?? "";
+    currentFarmVariety.value = data.farm_variety ?? null;
+    store.commit("home/SET_GARDEN_ID", data.id);
 };
 </script>
 
@@ -183,19 +206,34 @@ const changeGarden = ({ id }) => {
         z-index: 11;
     }
 
-    .weather-info {
-        width: calc(100% - 20px);
+    .agri-record-header {
         position: absolute;
-        top: 10px;
-        left: 10px;
         z-index: 12;
+        left: 10px;
+        top: 12px;
+        width: calc(100% - 20px);
+        transform: translateZ(0);
+
+        .weather-info {
+            width: 100%;
+            position: relative;
+            left: auto;
+            top: auto;
+
+            :deep(.garden-tabs) {
+                .garden-item.left-item.active .current-name {
+                    color: #2199F8;
+                    font-weight: 600;
+                }
+            }
+        }
     }
 
     .archives-time-line {
         position: relative;
         height: 100%;
         padding: 12px;
-        padding-top: 120px;
+        padding-top: 100px;
         display: flex;
         flex-direction: column;
         min-height: 0;

+ 477 - 0
src/views/old_mini/agri_record_detail/index.vue

@@ -0,0 +1,477 @@
+<template>
+    <div class="agri-record-detail">
+        <custom-header name="农事详情"></custom-header>
+        <div class="content">
+            <div class="status-banner" :class="`status-banner--${statusInfo.theme}`">
+                <div class="status-banner__title">{{ statusInfo.title }}</div>
+                <div class="status-banner__deadline">截止到 {{ statusInfo.deadline }}</div>
+            </div>
+            <div class="box-wrap execution-area-card">
+                <div class="execution-area-card__header">
+                    <span class="title">执行区域</span>
+                    <span class="code">{{ executionArea.zoneCode }}</span>
+                </div>
+                <div class="map-container" ref="mapContainer"></div>
+                <div class="execution-area-card__footer">
+                    <div class="address">
+                        <div class="address-title">{{ executionArea.addressTitle }}</div>
+                        <div class="address-detail">{{ executionArea.addressDetail }}</div>
+                    </div>
+                    <div class="nav-btn" @click="handleNavigate">导航到这里</div>
+                </div>
+            </div>
+            <div class="box-wrap work-summary-card">
+                <div class="header">
+                    <div class="icon">
+                        <el-icon color="#fff" :size="14">
+                            <School />
+                        </el-icon>
+                    </div>
+                    <span class="title">{{ workSummary.workName }}</span>
+                    <div class="tags">
+                        <span
+                            v-for="(tag, index) in workSummary.tags"
+                            :key="index"
+                            class="work-tag"
+                            :class="`work-tag--${tag.theme}`"
+                        >{{ tag.label }}</span>
+                    </div>
+                </div>
+                <div class="body">
+                    <div
+                        v-for="(row, index) in workSummary.detailRows"
+                        :key="index"
+                        class="detail-row"
+                        :class="{ 'detail-row--field': isFieldRow(row) }"
+                    >
+                        <span class="detail-row__label">{{ row.label }}</span>
+                        <span v-if="isTextRow(row)" class="detail-row__value">{{ formatRowValue(row) }}</span>
+                        <el-date-picker
+                            v-else-if="getEffectiveRowType(row) === 'date'"
+                            v-model="row.value"
+                            class="detail-row__control"
+                            type="date"
+                            format="YYYY.MM.DD"
+                            value-format="YYYY-MM-DD"
+                            placeholder="请选择日期"
+                            :editable="false"
+                            style="width: 100%"
+                        />
+                        <el-input
+                            v-else-if="getEffectiveRowType(row) === 'input'"
+                            v-model="row.value"
+                            class="detail-row__control"
+                            placeholder="请输入"
+                        />
+                    </div>
+                </div>
+            </div>
+            <div class="box-wrap">
+                <div class="info-item">
+                    <div class="info-title"><span class="title-block"></span>农事目的</div>
+                    <div class="info-value">农事目的农事目的农事目的农事目的农事目的农事目</div>
+                </div>
+                <div class="info-item">
+                    <div class="info-title"><span class="title-block"></span>药物处方</div>
+                    <div class="info-value">农事目的农事目的农事目的农事目的农事目的农事目</div>
+                </div>
+                <div class="info-item">
+                    <div class="info-title"><span class="title-block"></span>执行参数</div>
+                    <div class="info-value">农事目的农事目的农事目的农事目的农事目的农事目</div>
+                </div>
+                <div class="info-item">
+                    <div class="info-title"><span class="title-block"></span>注意事项</div>
+                    <div class="info-value">农事目的农事目的农事目的农事目的农事目的农事目</div>
+                </div>
+            </div>
+        </div>
+        <div class="custom-bottom-fixed-btns">
+            <div class="bottom-btn secondary-btn" @click="handleToggleEdit">
+                {{ isEditing ? '取消修改' : '修改信息' }}
+            </div>
+            <div class="bottom-btn primary-btn" :class="{ 'orange-btn': !isEditing }" @click="handleAccept">{{ isEditing ? '保存信息' : '我要接受' }}</div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref, onActivated, nextTick } from 'vue';
+import { School } from '@element-plus/icons-vue';
+import * as util from '@/common/ol_common.js';
+import customHeader from '@/components/customHeader.vue';
+import { useI18n } from '@/i18n';
+import IndexMap from './map/index.js';
+
+const { t } = useI18n();
+
+const DEFAULT_FARM_POINT = 'POINT(113.6142086995688 23.585836479509055)';
+const mapContainer = ref(null);
+const recordDetailMap = new IndexMap();
+
+const syncExecutionAreaMap = async () => {
+    const location = DEFAULT_FARM_POINT;
+    await nextTick();
+    if (!mapContainer.value) return;
+    if (recordDetailMap.kmap) {
+        const coordinate = util.wktCastGeom(location).getFirstCoordinate();
+        recordDetailMap.setMapPosition(coordinate);
+        recordDetailMap.kmap?.map?.updateSize?.();
+        return;
+    }
+    recordDetailMap.initMap(location, mapContainer.value);
+    recordDetailMap.kmap?.map?.updateSize?.();
+};
+
+onActivated(async () => {
+    isEditing.value = false;
+    detailRowsSnapshot = null;
+    await syncExecutionAreaMap();
+});
+
+const EDITABLE_FIELD_MAP = {
+    执行时间: 'date',
+    执行农机: 'input',
+};
+
+const isEditing = ref(false);
+let detailRowsSnapshot = null;
+
+const getEffectiveRowType = (row) => {
+    if (!isEditing.value) return undefined;
+    return EDITABLE_FIELD_MAP[row.label] || undefined;
+};
+
+const isTextRow = (row) => !getEffectiveRowType(row);
+const isFieldRow = (row) => !!getEffectiveRowType(row);
+
+const formatRowValue = (row) => {
+    if (!row?.value) return row?.value ?? '';
+    if (row.type === 'date' || row.label === '执行时间') {
+        return String(row.value).replace(/-/g, '.');
+    }
+    return row.value;
+};
+
+const normalizeDateValue = (value) => {
+    if (!value) return value;
+    return String(value).replace(/\./g, '-');
+};
+
+const normalizeDetailRows = (rows = []) =>
+    rows.map((row) => {
+        if (row.type === 'date' && row.value) {
+            return { ...row, value: normalizeDateValue(row.value) };
+        }
+        return row;
+    });
+
+const workSummary = ref({
+    workName: '农事名称',
+    tags: [
+        { label: '感知类', theme: 'orange' },
+        { label: '标准类', theme: 'blue' },
+        { label: '复核类', theme: 'green' },
+    ],
+    detailRows: normalizeDetailRows([
+        { label: '负责人', value: '张扬洋' },
+        { label: '农事详情', value: '解释解释解释解释' },
+        { label: '农情研判', value: '原因原因原因原因原因原因原因原因' },
+        { label: '执行时间', value: '2025.05.06' },
+        { label: '执行农机', value: 'D-260602-1455-01-01' },
+    ]),
+});
+
+const statusInfo = ref({
+    title: '待接收',
+    deadline: '2025.05.06',
+    theme: 'orange',
+});
+
+const executionArea = ref({
+    zoneCode: 'ws0gefwg9tdn',
+    addressTitle: '具体地址具体地址具体地',
+    addressDetail: '广东省广州市从化区富江道',
+});
+
+const handleNavigate = () => {};
+
+const handleToggleEdit = () => {
+    if (!isEditing.value) {
+        detailRowsSnapshot = JSON.parse(JSON.stringify(workSummary.value.detailRows));
+        workSummary.value.detailRows = workSummary.value.detailRows.map((row) => {
+            if (row.label === '执行时间' && row.value) {
+                return { ...row, value: normalizeDateValue(row.value) };
+            }
+            return row;
+        });
+        isEditing.value = true;
+        return;
+    }
+    if (detailRowsSnapshot) {
+        workSummary.value.detailRows = detailRowsSnapshot;
+        detailRowsSnapshot = null;
+    }
+    isEditing.value = false;
+};
+
+const handleAccept = () => {
+    // TODO: 提交接受农事接口
+    isEditing.value = false;
+    detailRowsSnapshot = null;
+};
+</script>
+
+<style scoped lang="scss">
+.agri-record-detail {
+    width: 100%;
+    height: 100vh;
+    display: flex;
+    flex-direction: column;
+    background: #f2f3f5;
+    overflow: hidden;
+
+    .content {
+        flex: 1;
+        min-height: 0;
+        width: 100%;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+        padding-bottom: 90px;
+        box-sizing: border-box;
+        
+        .status-banner {
+            padding: 20px 12px 72px;
+            color: #fff;
+
+            &--orange {
+                background: #FF9838;
+            }
+
+            &--blue {
+                background: #2199F8;
+            }
+
+            &--red {
+                background: #FF6A6A;
+            }
+
+            &__title {
+                font-size: 22px;
+            }
+        }
+
+        .box-wrap {
+            background: #ffffff;
+            border-radius: 8px;
+            padding: 10px;
+            margin: 10px 12px 0 12px;
+
+            &.execution-area-card {
+                position: relative;
+                z-index: 1;
+                margin-top: -52px;
+
+                .execution-area-card__header {
+                    display: flex;
+                    align-items: center;
+                    justify-content: space-between;
+                    .title {
+                        font-size: 16px;
+                    }
+                    .code {
+                        color: #565656;
+                    }
+                }
+
+                .map-container {
+                    width: 100%;
+                    height: 166px;
+                    margin: 12px 0;
+                    border-radius: 8px;
+                    overflow: hidden;
+                }
+
+                .execution-area-card__footer {
+                    display: flex;
+                    align-items: center;
+                    justify-content: space-between;
+                    .address {
+                        .address-title {
+                            font-size: 16px;
+                            color: #1D2129;
+                            // overflow: hidden;
+                            // text-overflow: ellipsis;
+                            // white-space: nowrap;
+                        }
+        
+                        .address-detail {
+                            margin-top: 2px;
+                            font-size: 12px;
+                            color: rgba(29, 33, 41, 0.6);
+                            // overflow: hidden;
+                            // text-overflow: ellipsis;
+                            // white-space: nowrap;
+                        }
+                    }
+                }
+
+                .nav-btn {
+                    padding: 6px 10px;
+                    border: 1px solid #2199F8;
+                    border-radius: 25px;
+                    color: #2199F8;
+                }
+            }
+
+            &.work-summary-card {
+                padding: 12px 10px 10px;
+
+                .header {
+                    display: flex;
+                    align-items: center;
+                    gap: 8px;
+                    border-bottom: 1px solid rgba(245, 245, 245, 0.99);
+                    padding-bottom: 8px;
+                    margin-bottom: 8px;
+                    .icon {
+                        width: 22px;
+                        height: 22px;
+                        border-radius: 4px;
+                        background: #2199F8;
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                        flex-shrink: 0;
+                    }
+    
+                    .title {
+                        font-size: 16px;
+                        font-weight: 500;
+                    }
+    
+                    .tags {
+                        display: flex;
+                        align-items: center;
+                        gap: 4px;
+                    }
+    
+                    .work-tag {
+                        padding: 0 6px;
+                        border-radius: 2px;
+                        font-size: 12px;
+    
+                        &--orange {
+                            background: rgba(255, 179, 47, 0.1);
+                            color: #FFB32F;
+                        }
+    
+                        &--blue {
+                            background: rgba(33, 153, 248, 0.1);
+                            color: #2199F8;
+                        }
+    
+                        &--green {
+                            background: rgba(58, 173, 148, 0.1);
+                            color: #3AAD94;
+                        }
+                    }
+                }
+
+                .body {
+                    display: flex;
+                    flex-direction: column;
+                    gap: 6px;
+                    .detail-row {
+                        display: flex;
+                        gap: 8px;
+    
+                        &__label {
+                            width: 56px;
+                            flex-shrink: 0;
+                            color: rgba(111, 114, 116, 0.6);
+                        }
+    
+                        &__value {
+                            color: #565656;
+                        }
+
+                        &__control {
+                            flex: 1;
+                            min-width: 0;
+                        }
+
+                        &--field {
+                            align-items: center;
+
+                            :deep(.el-input__wrapper) {
+                                border: 1px solid rgba(33, 153, 248, 0.5);
+                                border-radius: 4px;
+                                box-shadow: none;
+                            }
+
+                            :deep(.el-input__inner) {
+                                color: #2199F8;
+                            }
+
+                            :deep(.el-input__prefix),
+                            :deep(.el-input__suffix) {
+                                color: #2199F8;
+                            }
+                        }
+                    }
+                }
+
+            }
+
+            .info-item {
+                .info-title {
+                    display: flex;
+                    align-items: center;
+                    gap: 4px;
+                    color: #2199F8;
+                    font-weight: 500;
+
+                    .title-block {
+                        width: 6px;
+                        height: 6px;
+                        background: #2199F8;
+                        border-radius: 1px;
+                        transform: rotate(45deg);
+                    }
+                }
+
+                .info-value {
+                    padding-top: 4px;
+                    color: #ACACAC;
+                    line-height: 21px;
+                    white-space: pre-line;
+                }
+
+                .blod-text {
+                    font-weight: 500;
+                }
+            }
+
+            .info-item+.info-item {
+                margin-top: 10px;
+            }
+        }
+    }
+
+    .custom-bottom-fixed-btns {
+        justify-content: space-between;
+        .secondary-btn{
+            background: #F6F6F6;
+            border: none;
+        }
+        .primary-btn{
+            background: #2199F8;
+            border: none;
+        }
+        .orange-btn{
+            background: #FF953D;
+            color: #fff;
+        }
+    }
+}
+</style>

+ 97 - 0
src/views/old_mini/agri_record_detail/map/index.js

@@ -0,0 +1,97 @@
+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 { reactive } from "vue";
+
+export let mapLocation = reactive({
+  data: null,
+});
+
+/**
+ * @description 地图层对象
+ */
+class IndexMap {
+  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/map/map_point.png"),
+            scale: 0.5,
+          }),
+        });
+      },
+    });
+  }
+
+  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.addMapSingerClick()
+  }
+
+  setMapPosition(center) {
+    this.kmap.getView().animate({
+      center,
+      zoom: 16,
+      duration: 0,
+    });
+    this.setMapPoint(center)
+  }
+
+  setMapPoint(coordinate) {
+    this.clickPointLayer.source.clear()
+    let point = new Feature(new Point(coordinate))
+    this.clickPointLayer.addFeature(point)
+  }
+
+  // 地图点击事件
+  addMapSingerClick() {
+    let that = this;
+    that.kmap.on("singleclick", (evt) => {
+      // that.kmap.map.forEachFeatureAtPixel(evt.pixel, function (feature, layer) {
+      //   if ( layer instanceof VectorLayer && layer.get("name") === "reportPolygonLayer" ) {
+      //     areaId.data = feature.get("id")
+      //   }
+      // });
+      that.setMapPoint(evt.coordinate)
+      mapLocation.data = evt.coordinate
+    });
+  }
+
+  clearLayer() {
+    // this.kmap.removeLayer(this.clickPointLayer.layer)
+    this.kmap.polygonLayer.source.clear();
+  }
+
+  setAreaGeometry(geometryArr) {
+    this.clearLayer()
+    let that = this
+    geometryArr.map(item => {
+      that.kmap.setLayerWkt(item)
+    })
+    this.fitView()
+  }
+
+  fitView(){
+    let extent = this.kmap.polygonLayer.source.getExtent()
+    // 地图自适应到区域可视范围
+    this.kmap.getView().fit(extent, { duration: 500, padding: [100, 100, 100, 100] });
+  }
+}
+
+export default IndexMap;