Prechádzať zdrojové kódy

feat:添加长势异常逻辑

wangsisi 2 týždňov pred
rodič
commit
47cc6eafd2

+ 14 - 0
src/i18n/recordDetails-messages.js

@@ -7,6 +7,13 @@ export const recordDetailsZh = {
     phenotypeLabel: "表型特征:",
     highRiskLabel: "高发区域:",
     harmLevelLabel: "危害等级:",
+    anomalyLevelLabel: "异常等级:",
+    growthAbnormalBanner: "点击记录长势异常动态",
+    growthAbnormalBannerDesc: "生成精细处方图",
+    growthAbnormalRecord: "点击记录",
+    growthAbnormalTypeLabel: "请选择发生的异常类型",
+    growthAbnormalTypePlaceholder: "请选择异常类型",
+    growthAbnormalTypeRequired: "请选择异常类型",
     farmAnalysisLabel: "农情研判:",
     patrolLabel: "巡园要点:",
     growthQuestion: "果径是否显著小于正常果,且果皮失去光泽、呈现暗哑状态?",
@@ -98,6 +105,13 @@ export const recordDetailsEn = {
     phenotypeLabel: "Phenotype: ",
     highRiskLabel: "High-risk areas: ",
     harmLevelLabel: "Damage level: ",
+    anomalyLevelLabel: "Abnormality level: ",
+    growthAbnormalBanner: "Record growth anomaly updates",
+    growthAbnormalBannerDesc: "Generate prescription map",
+    growthAbnormalRecord: "Record",
+    growthAbnormalTypeLabel: "Select abnormality type",
+    growthAbnormalTypePlaceholder: "Select abnormality type",
+    growthAbnormalTypeRequired: "Please select abnormality type",
     farmAnalysisLabel: "Assessment: ",
     patrolLabel: "Patrol points: ",
     growthQuestion:

+ 220 - 27
src/views/old_mini/recordDetails/index.vue

@@ -27,17 +27,23 @@
                             <span>{{ t('recordDetails.abnormalRecord') }}</span>
                         </div>
                     </div>
-                    <div v-if="recordType === 'growth' && !showMap" class="tabs-list">
-                        <div v-for="(item, index) in growthAnomalyTabs" :key="index" class="item-tab"
-                            :class="{ 'item-tab--active': activeGrowthAnomalyIndex === index }"
-                            @click="handleGrowthAnomalyTabClick(index)">{{ item.label }}</div>
+                    <div v-if="recordType === 'growth'" class="phenology-tip-banner">
+                        <div class="banner__left">
+                            <div class="banner__title">
+                                <span>{{ t('recordDetails.growthAbnormalBanner') }}</span>
+                            </div>
+                            <span class="banner__desc">{{ t('recordDetails.growthAbnormalBannerDesc') }}</span>
+                        </div>
+                        <div class="banner__btn" @click="handleAbnormalRecord">
+                            <span>{{ t('recordDetails.growthAbnormalRecord') }}</span>
+                        </div>
                     </div>
-                    <template v-if="recordType !== 'pest'">
+                    <template v-if="recordType === 'phenology'">
                         <div v-for="field in trailDetailFields" :key="field.labelKey" class="card-item">
                             <span class="item-label">{{ t(`recordDetails.${field.labelKey}`) }}</span>
                             <text-ellipsis class="item-value" v-bind="textEllipsisBind" :content="field.content" />
                         </div>
-                        <div v-if="recordType === 'phenology'" class="phenology-tip-banner blue">
+                        <div class="phenology-tip-banner blue">
                             <div class="banner__left">
                                 <div class="banner__title">{{ t('recordDetails.partitionBannerTitle') }}</div>
                                 <span class="banner__desc">{{ t('recordDetails.partitionBannerDesc') }}</span>
@@ -48,6 +54,39 @@
                         </div>
                     </template>
                 </div>
+                <template v-if="recordType === 'growth' && !showMap">
+                    <div class="growth-anomaly-tabs">
+                        <div v-for="(item, index) in growthAnomalyTabs" :key="index" class="item-tab"
+                            :class="{ 'item-tab--active': activeGrowthAnomalyIndex === index }"
+                            @click="handleGrowthAnomalyTabClick(index)">
+                            <span v-if="getGrowthTabDisplayLevel(index) > 0" class="item-tab__badge" :class="[
+                                `item-tab__badge--${getGrowthTabDisplayLevel(index)}`,
+                                { 'item-tab__badge--on-active': activeGrowthAnomalyIndex === index },
+                            ]">
+                                {{ t('agriRecord.riskLevel', { level: getGrowthTabDisplayLevel(index) }) }}
+                            </span>
+                            <span class="item-tab__text">{{ item.label }}</span>
+                        </div>
+                    </div>
+                    <div class="card-wrap growth-detail-card">
+                        <div v-for="field in trailDetailFields" :key="field.labelKey" class="card-item"
+                            :class="{ 'card-item--phenotype': field.image }">
+                            <div v-if="field.image" class="card-item__phenotype">
+                                <div class="card-item__phenotype-content">
+                                    <span class="item-label">{{ t(`recordDetails.${field.labelKey}`) }}</span>
+                                    <text-ellipsis class="item-value" v-bind="textEllipsisBind"
+                                        :content="field.content" />
+                                </div>
+                                <img class="card-item__phenotype-image" :src="field.image" alt=""
+                                    @click="handlePhenotypeImagePreview(field.image)" />
+                            </div>
+                            <template v-else>
+                                <span class="item-label">{{ t(`recordDetails.${field.labelKey}`) }}</span>
+                                <text-ellipsis class="item-value" v-bind="textEllipsisBind" :content="field.content" />
+                            </template>
+                        </div>
+                    </div>
+                </template>
                 <div v-if="recordType === 'pest'" class="pest-classify-picker">
                     <div class="pest-classify-picker__tabs" ref="pestTabsRef">
                         <div v-for="(label, i) in pestTopLabels" :key="'top-' + i" :ref="(el) => setPestTabRef(el, i)"
@@ -151,8 +190,9 @@
         <ImagePreviewPopup v-model:show="showPhenotypeImagePreview" :images="phenotypePreviewImages" />
 
         <!-- 物候 上传弹窗 -->
-        <UploadProgressPopup ref="phenologyUploadPopupRef" v-model:show="showPhenologyUploadPopup" :enable-identify="false"
-            @cancel="handleCancelPhenologyUploadPopup" :upload-required="false" @confirm="handleConfirmPhenologyUpload">
+        <UploadProgressPopup ref="phenologyUploadPopupRef" v-model:show="showPhenologyUploadPopup"
+            :enable-identify="false" @cancel="handleCancelPhenologyUploadPopup" :upload-required="false"
+            @confirm="handleConfirmPhenologyUpload">
             <template #header>
                 <div class="upload-progress-title">
                     <!-- <span class="label">{{ $t('当前现状:') }}</span> -->
@@ -163,12 +203,21 @@
 
         <!-- 病虫害 上传弹窗 -->
         <UploadProgressPopup ref="pestUploadPopupRef" v-model:show="showPestUploadPopup" :enable-identify="true"
-            :init-img-arr="pestInitImgArr"
-            :confirm-text="t('recordDetails.confirmUpload')" :upload-required="false"
+            :init-img-arr="pestInitImgArr" :confirm-text="t('recordDetails.confirmUpload')" :upload-required="false"
             @reset="handlePestUploadPopupReset" @cancel="handleCancelPestUploadPopup"
             @confirm="handleConfirmPestUpload">
             <template #header>
                 <div class="upload-form">
+                    <div v-if="recordType === 'growth'" class="form-item growth-abnormal-type-field">
+                        <div class="growth-abnormal-type-field__label">
+                            {{ t('recordDetails.growthAbnormalTypeLabel') }}
+                        </div>
+                        <el-select v-model="formData.abnormalType" class="growth-abnormal-type-select"
+                            placeholder="请选择异常类型" size="large">
+                            <el-option v-for="item in growthAbnormalTypeOptions" :key="item.value" :label="item.label"
+                                :value="item.value" />
+                        </el-select>
+                    </div>
                     <div class="form-item special-input">
                         <div class="item-label" style="font-size: 14px;color: #5A5A5A;">
                             <span>{{ t('recordDetails.abnormalCount') }}</span>
@@ -395,12 +444,25 @@ const tabsList = ref([
     { label: '分区四', value: 3 },
 ]);
 
+/** UI 占位:接口未返回等级时,前三个 Tab 依次展示三级 / 二级 / 一级 */
+const GROWTH_TAB_MOCK_LEVELS = [3, 2, 1];
+
+function getGrowthTabDisplayLevel(index) {
+    return GROWTH_TAB_MOCK_LEVELS[index] ?? 0;
+}
+
+function resolveGrowthAnomalyOptionValue(item, index) {
+    const raw = item?.value ?? item?.id ?? item?.code ?? index;
+    if (raw === undefined || raw === null || raw === '') return '';
+    return String(raw);
+}
+
 const activeGrowthAnomalyIndex = ref(0);
 const growthAnomalyData = ref([]);
 const growthAnomalyTabs = computed(() =>
     growthAnomalyData.value.map((item, index) => ({
         label: item.name ?? '',
-        value: item.value ?? item.id ?? index,
+        value: resolveGrowthAnomalyOptionValue(item, index),
     }))
 );
 
@@ -434,8 +496,35 @@ const hasMapConfirmGeometry = computed(() => store.getters['recordDetails/mapCon
 const formData = ref({
     ratio: '',
     regionName: '',
+    abnormalType: '',
 });
 
+const growthAbnormalTypeOptions = computed(() => {
+    if (growthAnomalyData.value.length) {
+        return growthAnomalyData.value.map((item, index) => ({
+            label: item.name ?? '',
+            value: resolveGrowthAnomalyOptionValue(item, index),
+        }));
+    }
+    return [
+        { label: t('recordDetails.tabFruitBlocked'), value: 'fruit_blocked' },
+        { label: t('recordDetails.tabSlowGrowth'), value: 'slow_growth' },
+        { label: t('recordDetails.tabVigorous'), value: 'vigorous' },
+        { label: t('recordDetails.tabWilting'), value: 'wilting' },
+    ];
+});
+
+function syncGrowthAbnormalTypeSelection() {
+    const idx = activeGrowthAnomalyIndex.value;
+    const fromApi = resolveGrowthAnomalyOptionValue(growthAnomalyData.value[idx], idx);
+    const options = growthAbnormalTypeOptions.value;
+    if (fromApi && options.some((o) => o.value === fromApi)) {
+        formData.value.abnormalType = fromApi;
+        return;
+    }
+    formData.value.abnormalType = options[idx]?.value ?? options[0]?.value ?? '';
+}
+
 function destroyMap() {
     if (indexMap.kmap?.destroy) {
         indexMap.kmap.destroy();
@@ -496,13 +585,18 @@ function syncFormFromMapConfirmPayload() {
 const handleAbnormalRecord = () => {
     formData.value.ratio = '';
     formData.value.regionName = '';
+    if (recordType.value === 'growth') {
+        syncGrowthAbnormalTypeSelection();
+    } else {
+        formData.value.abnormalType = '';
+    }
     showPestUploadPopup.value = true;
 };
 
 const handleMapClick = () => {
     router.push({
         name: 'MapManage', query: {
-            type: 'pest',
+            type: recordType.value,
         }
     });
 }
@@ -512,6 +606,9 @@ const showPestUploadPopup = ref(false);
 
 watch(showPestUploadPopup, (show) => {
     if (show) {
+        if (recordType.value === 'growth') {
+            nextTick(() => syncGrowthAbnormalTypeSelection());
+        }
         syncUploadMapView();
     } else {
         destroyMap();
@@ -591,8 +688,17 @@ function validateAbnormalRatio() {
     return true;
 }
 
+function validateGrowthAbnormalType() {
+    if (String(formData.value.abnormalType ?? '').trim() === '') {
+        ElMessage.warning(t('recordDetails.growthAbnormalTypeRequired'));
+        return false;
+    }
+    return true;
+}
+
 const handleConfirmPestUpload = (imgArr) => {
     if (!validateAbnormalRatio()) return;
+    if (recordType.value === 'growth' && !validateGrowthAbnormalType()) return;
 
     if (recordType.value === 'pest') {
         const hasDrawnMap = store.getters['recordDetails/mapConfirmCoordinatesArray'].length > 0;
@@ -675,9 +781,28 @@ const leadDetailField = computed(() => {
 
 const trailDetailFields = computed(() => {
     if (recordType.value === 'growth') {
+        const detail = currentGrowthAnomalyDetail.value;
+        const phenotypeImage = detail?.phenotype_image ?? detail?.phenotype_img ?? detail?.image;
+        const mockLevel = getGrowthTabDisplayLevel(activeGrowthAnomalyIndex.value);
+        const anomalyLevelContent =
+            detail?.abnormal_level ??
+            detail?.anomaly_level ??
+            detail?.level_desc ??
+            detail?.harm_level ??
+            detail?.level;
         return [
-            { labelKey: 'phenotypeLabel', content: currentGrowthAnomalyDetail.value?.phenotype },
-            { labelKey: 'highRiskLabel', content: currentGrowthAnomalyDetail.value?.patrol_points },
+            {
+                labelKey: 'anomalyLevelLabel',
+                content:
+                    anomalyLevelContent ||
+                    (mockLevel ? t('agriRecord.riskLevel', { level: mockLevel }) : ''),
+            },
+            {
+                labelKey: 'phenotypeLabel',
+                content: detail?.phenotype,
+                image: phenotypeImage || pestPhenotypePlaceholder,
+            },
+            { labelKey: 'highRiskLabel', content: detail?.patrol_points },
         ];
     }
     if (recordType.value === 'pest') {
@@ -939,28 +1064,73 @@ const getFindPhenologyInfo = async () => {
             .tabs-list {
                 margin: 10px 0;
                 flex-shrink: 0;
-                display: grid;
-                grid-template-columns: repeat(auto-fill, minmax(8em, 1fr));
+            }
+
+            .card-wrap+.phenology-tip-banner,
+            .phenology-tip-banner+.growth-anomaly-tabs,
+            .growth-anomaly-tabs+.card-wrap {
+                margin-top: 10px;
+            }
+
+            .growth-anomaly-tabs {
+                display: flex;
+                flex-wrap: wrap;
                 gap: 8px;
+                padding-top: 12px;
 
                 .item-tab {
+                    position: relative;
+                    flex: 1 1 calc(33.333% - 6px);
+                    min-width: 0;
                     box-sizing: border-box;
-                    padding: 6px 4px;
-                    border-radius: 2px;
-                    color: #767676;
+                    padding: 8px 0;
+                    border: 1px solid transparent;
+                    border-radius: 6px;
                     background: #fff;
-                    display: flex;
-                    align-items: center;
-                    justify-content: center;
+                    color: #666;
                     text-align: center;
-                    font-size: 13px;
-                    white-space: nowrap;
-                    word-break: keep-all;
+                    font-size: 14px;
+                    font-weight: 600;
                 }
 
                 .item-tab--active {
-                    background: #2199F8;
-                    color: #ffffff;
+                    border-color: #2199f8;
+                    background: rgba(33, 153, 248, 0.08);
+                    color: #2199f8;
+                }
+
+                .item-tab__text {
+                    display: block;
+                    width: 100%;
+                }
+
+                .item-tab__badge {
+                    position: absolute;
+                    top: -9px;
+                    right: -8px;
+                    padding: 0 6px;
+                    border-radius: 2px;
+                    font-size: 10px;
+                    font-weight: 400;
+                    line-height: 16px;
+                    color: #fff;
+                    z-index: 1;
+
+                    &--1 {
+                        background: rgba(33, 153, 248, 0.4);
+                    }
+
+                    &--2 {
+                        background: rgba(33, 153, 248, 0.7);
+                    }
+
+                    &--3 {
+                        background: #2199f8;
+                    }
+
+                    &--on-active {
+                        background: #2199f8;
+                    }
                 }
             }
 
@@ -1349,6 +1519,29 @@ const getFindPhenologyInfo = async () => {
         }
     }
 
+    .growth-abnormal-type-field {
+        margin-bottom: 12px;
+
+        &__label {
+            font-size: 16px;
+            color: #000;
+            margin-bottom: 10px;
+        }
+    }
+
+    .growth-abnormal-type-select {
+        width: 100%;
+
+        :deep(.el-select__wrapper) {
+            min-height: 44px;
+            padding: 4px 12px;
+            border-radius: 6px;
+            border: 1px solid #d6d6d6;
+            box-shadow: none;
+            background: #fff;
+        }
+    }
+
     .special-input {
         padding: 8px;
         background: rgba(33, 153, 248, 0.1);

+ 17 - 9
src/views/old_mini/recordDetails/map/mapManage.js

@@ -24,6 +24,12 @@ import * as proj from "ol/proj";
 import { getArea } from "ol/sphere.js";
 import * as turf from "@turf/turf";
 
+const DEFAULT_ZONE_STYLE = {
+  fill: "rgba(100, 0, 0, 0.45)",
+  fillSelected: "rgba(100, 0, 0, 0.5)",
+  stroke: "#E03131",
+};
+
 const VIEWPORT_INTERACTION_TYPES = [
   DragPan,
   MouseWheelZoom,
@@ -72,15 +78,16 @@ class MapManage {
     this.boundaryGeometry = null;
     this.constrainedDrawing = false;
     this.constrainedDrawingReady = false;
+    this.zoneStyle = { ...DEFAULT_ZONE_STYLE };
     this.gridLayer = new KMap.VectorLayer("terrainGridLayer", 1100, {
       style: (feature) => {
         const selected = !!feature.get("selected");
         return new Style({
           fill: new Fill({
-            color: selected ? "rgba(100, 0, 0, 0.5)" : "rgba(255, 255, 255, 0.01)",
+            color: selected ? this.zoneStyle.fillSelected : "rgba(255, 255, 255, 0.01)",
           }),
           stroke: new Stroke({
-            color: selected ? "#E03131" : "#fff",
+            color: selected ? this.zoneStyle.stroke : "#fff",
             width: selected ? 1.8 : 1.2,
           }),
         });
@@ -118,20 +125,21 @@ class MapManage {
     });
   }
 
-  createDrawnPestStyle() {
+  createDrawnZoneStyle() {
     return new Style({
       fill: new Fill({
-        color: "rgba(100, 0, 0, 0.45)",
+        color: this.zoneStyle.fill,
       }),
       stroke: new Stroke({
-        color: "#E03131",
+        color: this.zoneStyle.stroke,
         width: 1.8,
       }),
     });
   }
 
   initMap(location, target, options = {}) {
-    const { editable = true, constrainedDrawing = false, onDrawOutsideBoundary } = options;
+    const { editable = true, constrainedDrawing = false, onDrawOutsideBoundary, zoneStyle } = options;
+    this.zoneStyle = zoneStyle ? { ...DEFAULT_ZONE_STYLE, ...zoneStyle } : { ...DEFAULT_ZONE_STYLE };
     this.editable = editable;
     this.constrainedDrawing = constrainedDrawing;
     this.onDrawOutsideBoundary = typeof onDrawOutsideBoundary === "function" ? onDrawOutsideBoundary : null;
@@ -308,7 +316,7 @@ class MapManage {
       return false;
     }
     feature.setGeometry(clipped);
-    feature.setStyle(this.createDrawnPestStyle());
+    feature.setStyle(this.createDrawnZoneStyle());
     return true;
   }
 
@@ -492,10 +500,10 @@ class MapManage {
 
     const selectedStyle = new Style({
       fill: new Fill({
-        color: "rgba(100, 0, 0, 0.5)",
+        color: this.zoneStyle.fillSelected,
       }),
       stroke: new Stroke({
-        color: "#E03131",
+        color: this.zoneStyle.stroke,
         width: 1.8,
       }),
     });

+ 38 - 37
src/views/old_mini/recordDetails/mapManage.vue

@@ -4,24 +4,24 @@
         <div class="map-manage-content">
             <!-- <locationSearch class="location-search" @change="handleLocationChange"></locationSearch> -->
             <div class="map-container" ref="mapContainer"></div>
-            <div class="map-tip" v-if="recordType === 'pest'">{{ $t('请在虚线管理区域内勾画病虫害地块,区域外不可勾画') }}</div>
-            <div class="new-region-btn" v-show="!drawingEnabled && recordType !== 'pest'" @click="onStartRegionDrawing">
+            <div class="map-tip" v-if="isConstrainedZoneMode">
+                {{ recordType === 'growth'
+                    ? $t('建立您的专属长势异常地图档案,生成分区靶向治疗方案')
+                    : $t('建立您的专属病虫害地图档案,生成分区靶向治疗方案') }}
+            </div>
+            <div class="new-region-btn" v-show="!drawingEnabled && !isConstrainedZoneMode" @click="onStartRegionDrawing">
                 {{ $t('新建管理分区') }}</div>
-            <div class="map-icon" v-show="drawingEnabled && recordType !== 'pest'" @click="handleMapIconClick">
+            <div class="map-icon" v-show="drawingEnabled && !isConstrainedZoneMode" @click="handleMapIconClick">
                 <img src="@/assets/img/map/map-icon.png" alt="">
             </div>
-            <div class="map-legend" v-if="recordType === 'pest'">
-                <div class="map-legend__item">
-                    <span class="map-legend__line map-legend__line--boundary"></span>
-                    <span class="map-legend__text">{{ $t('管理区域') }}</span>
-                </div>
+            <div class="map-legend" :class="{ 'map-legend--growth': recordType === 'growth' }" v-if="isConstrainedZoneMode">
                 <div class="map-legend__item">
                     <span class="map-legend__dot map-legend__dot--past"></span>
-                    <span class="map-legend__text">{{ $t('过往病虫害') }}</span>
+                    <span class="map-legend__text">{{ recordType === 'growth' ? $t('过往长势异常') : $t('过往病虫害') }}</span>
                 </div>
                 <div class="map-legend__item">
                     <span class="map-legend__dot map-legend__dot--zone"></span>
-                    <span class="map-legend__text">{{ $t('病虫害区域') }}</span>
+                    <span class="map-legend__text">{{ recordType === 'growth' ? $t('长势异常区域') : $t('病虫害区域') }}</span>
                 </div>
             </div>
         </div>
@@ -30,7 +30,7 @@
                 <div class="bottom-btn secondary-btn" @click="handleClearDraw">{{ $t('取消勾选') }}</div>
                 <div class="bottom-btn primary-btn" @click="handleConfirmDraw">{{ $t('确认区域') }}</div>
             </template>
-            <div v-else-if="recordType === 'pest'" class="bottom-btn primary-btn" @click="handleConfirmDraw">{{
+            <div v-else-if="isConstrainedZoneMode" class="bottom-btn primary-btn" @click="handleConfirmDraw">{{
                 $t('确认区域') }}</div>
             <div v-else class="bottom-btn secondary-btn">{{ $t('邀请勾画') }}</div>
         </div>
@@ -81,6 +81,12 @@ const router = useRouter();
 const route = useRoute();
 const store = useStore();
 const recordType = computed(() => route.query.type);
+const isConstrainedZoneMode = computed(() => recordType.value === 'pest' || recordType.value === 'growth');
+const zoneStyle = computed(() => (
+    recordType.value === 'growth'
+        ? { fill: 'rgba(124, 46, 0, 0.5)', fillSelected: 'rgba(124, 46, 0, 0.5)', stroke: '#FF7300' }
+        : undefined
+));
 const mapManage = new MapManage();
 
 const DEFAULT_MAP_LOCATION = "POINT(113.6142086995688 23.585836479509055)";
@@ -165,9 +171,9 @@ const confirmPayload = ref({
     selectedGridIds: [],
 });
 
-/** 病虫害:仅区域名称弹窗确认后才写入 store */
-const pestMapPayloadCommitted = ref(false);
-const pestMapStoreSnapshot = ref(null);
+/** 病虫害/长势异常:仅区域名称弹窗确认后才写入 store */
+const constrainedZoneMapPayloadCommitted = ref(false);
+const constrainedZoneMapStoreSnapshot = ref(null);
 
 function buildMapConfirmPayload(overrides = {}) {
     const selectedGrids = mapManage.getSelectedGrids();
@@ -189,7 +195,7 @@ function buildMapConfirmPayload(overrides = {}) {
 
 const handleRegionNameConfirm = (regionName) => {
     confirmPayload.value.zone_name = regionName;
-    pestMapPayloadCommitted.value = true;
+    constrainedZoneMapPayloadCommitted.value = true;
     store.commit('recordDetails/SET_MAP_CONFIRM_PAYLOAD', buildMapConfirmPayload({
         zone_name: regionName,
     }));
@@ -219,7 +225,7 @@ const handleClearDraw = () => {
 };
 
 const handleConfirmDraw = () => {
-    if (recordType.value === 'pest') {
+    if (isConstrainedZoneMode.value) {
         const payload = mapManage.getAreaGeometry();
         if (!payload.geometryArr.length) {
             ElMessage.warning("请先勾画地块");
@@ -238,9 +244,10 @@ function initMapManageView() {
     if (!mapContainer.value) return;
     mapManage.destroyMap();
     mapManage.initMap(getFarmMapLocation(), mapContainer.value, {
-        editable: recordType.value !== 'pest',
-        constrainedDrawing: recordType.value === 'pest',
-        onDrawOutsideBoundary: recordType.value === 'pest'
+        editable: !isConstrainedZoneMode.value,
+        constrainedDrawing: isConstrainedZoneMode.value,
+        zoneStyle: zoneStyle.value,
+        onDrawOutsideBoundary: isConstrainedZoneMode.value
             ? () => ElMessage.warning('请在虚线管理区域内勾画')
             : undefined,
     });
@@ -253,7 +260,7 @@ function initMapManageView() {
             gridMultipolygon: stored.gridMultipolygon || '',
             selectedGridIds: Array.isArray(stored.selectedGridIds) ? [...stored.selectedGridIds] : [],
         };
-        if (recordType.value !== 'pest') {
+        if (!isConstrainedZoneMode.value) {
             const geometryArr = store.getters['recordDetails/mapConfirmCoordinatesArray'];
             if (geometryArr.length) {
                 mapManage.setAreaGeometry(geometryArr);
@@ -269,7 +276,7 @@ function initMapManageView() {
         };
     }
 
-    if (recordType.value === 'pest') {
+    if (isConstrainedZoneMode.value) {
         mapManage.enableMapInteraction();
         if (confirmPayload.value.gridMultipolygon) {
             mapManage.setBoundaryWkt(confirmPayload.value.gridMultipolygon);
@@ -300,10 +307,10 @@ function initMapManageView() {
 }
 
 onActivated(() => {
-    pestMapPayloadCommitted.value = false;
-    if (recordType.value === 'pest') {
+    constrainedZoneMapPayloadCommitted.value = false;
+    if (isConstrainedZoneMode.value) {
         const stored = store.state.recordDetails.mapConfirmPayload;
-        pestMapStoreSnapshot.value = {
+        constrainedZoneMapStoreSnapshot.value = {
             zone_name: stored.zone_name || '',
             coordinates: stored.coordinates,
             gridMultipolygon: stored.gridMultipolygon || '',
@@ -314,13 +321,13 @@ onActivated(() => {
 });
 
 onDeactivated(() => {
-    if (recordType.value === 'pest' && !pestMapPayloadCommitted.value && pestMapStoreSnapshot.value) {
-        store.commit('recordDetails/SET_MAP_CONFIRM_PAYLOAD', { ...pestMapStoreSnapshot.value });
+    if (isConstrainedZoneMode.value && !constrainedZoneMapPayloadCommitted.value && constrainedZoneMapStoreSnapshot.value) {
+        store.commit('recordDetails/SET_MAP_CONFIRM_PAYLOAD', { ...constrainedZoneMapStoreSnapshot.value });
     }
     mapManage.destroyMap();
     drawingEnabled.value = false;
-    pestMapPayloadCommitted.value = false;
-    pestMapStoreSnapshot.value = null;
+    constrainedZoneMapPayloadCommitted.value = false;
+    constrainedZoneMapStoreSnapshot.value = null;
 });
 </script>
 
@@ -425,15 +432,9 @@ onDeactivated(() => {
                 }
             }
 
-            &__line {
-                display: inline-block;
-                width: 18px;
-                height: 0;
-                border-top: 2px dashed rgba(255, 255, 255, 0.85);
-                box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15);
-
-                &--boundary {
-                    flex-shrink: 0;
+            &--growth {
+                .map-legend__dot--past {
+                    background: rgba(124, 46, 0, 0.3);
                 }
             }
         }