소스 검색

fix: 录入信息,新增相册

lxf 4 일 전
부모
커밋
fdeedb8777

+ 52 - 1
src/views/old_mini/agri_file/pages/addZone.vue

@@ -40,6 +40,7 @@ const { t } = useI18n();
 const route = useRoute();
 const router = useRouter();
 const isFarmDrawMode = computed(() => route.query.type === "farm");
+const isRegionDrawMode = computed(() => route.query.type === "region");
 const targetUrl = computed(() => route.query.targetUrl);
 const isAbnormal = computed(() => {
     const val = route.query.isAbnormal;
@@ -59,6 +60,48 @@ const ZONE_STYLE = {
     stroke: "#2199F8",
 };
 
+/** 新增相册时展示的已有品种种植范围(mock,中心点附近) */
+const REGION_CENTER = [113.31812938867188, 23.002427029296875];
+const MOCK_VARIETY_ZONE_DEFS = [
+    {
+        id: 1,
+        name: "桂味",
+        dlng: -0.0032,
+        dlat: 0.0021,
+        delta: 0.00115,
+    },
+    {
+        id: 2,
+        name: "糯米糍",
+        dlng: 0.0028,
+        dlat: 0.0016,
+        delta: 0.00105,
+    },
+];
+
+function squarePolygonWkt(lng, lat, delta = 0.001) {
+    const ring = [
+        [lng - delta, lat + delta * 0.7],
+        [lng + delta * 0.35, lat + delta],
+        [lng + delta, lat - delta * 0.2],
+        [lng + delta * 0.15, lat - delta],
+        [lng - delta * 0.85, lat - delta * 0.45],
+        [lng - delta, lat + delta * 0.7],
+    ];
+    return `POLYGON((${ring.map((point) => point.join(" ")).join(", ")}))`;
+}
+
+function getMockVarietyZones() {
+    const [lng, lat] = REGION_CENTER;
+    return MOCK_VARIETY_ZONE_DEFS.map((item) => ({
+        id: item.id,
+        name: item.name,
+        fill: 'rgba(0, 0, 0, 0.38)',
+        stroke: 'rgba(200, 200, 200, 0.55)',
+        polygon: squarePolygonWkt(lng + item.dlng, lat + item.dlat, item.delta),
+    }));
+}
+
 const userLocation = ref(
     store.state.home.miniUserLocation ||
     localStorage.getItem("MINI_USER_LOCATION") ||
@@ -132,7 +175,7 @@ function getFarmDrawCoordinate() {
 }
 
 function getMapLocation() {
-    if (isFarmDrawMode.value) return FARM_DRAW_LOCATION;
+    if (isFarmDrawMode.value || isRegionDrawMode.value) return FARM_DRAW_LOCATION;
     return getDefaultMapLocation();
 }
 
@@ -147,6 +190,10 @@ function initMapView() {
     if (isFarmDrawMode.value) {
         mapManage.showCenterMarker(getFarmDrawCoordinate(), POINT_ICON.plant);
     }
+    if (isRegionDrawMode.value) {
+        mapManage.setExistingVarietyZones(getMockVarietyZones());
+        mapManage.setMapPosition(getFarmDrawCoordinate());
+    }
 }
 
 const handleLocationChange = (payload) => {
@@ -161,6 +208,10 @@ const handleLocate = () => {
         mapManage.showCenterMarker(coordinate, POINT_ICON.plant);
         return;
     }
+    if (isRegionDrawMode.value) {
+        mapManage.setMapPosition(getFarmDrawCoordinate());
+        return;
+    }
     mapManage.setMapPosition(getDefaultMapCoordinate());
 };
 

+ 1 - 1
src/views/old_mini/agri_file/pages/growthTrack.vue

@@ -137,7 +137,7 @@ const handleSubmit = () => {
     ElMessage.success(t("agriFile.submitSuccess"));
 };
 
-const needDraw = ref(true);
+const needDraw = ref(false);
 const handleUploadClick = () => {
     if (needDraw.value) {
         router.push({

+ 143 - 0
src/views/old_mini/entry_information/components/addMachinePopup.vue

@@ -0,0 +1,143 @@
+<template>
+    <popup
+        v-model:show="showValue"
+        round
+        closeable
+        class="add-machine-popup"
+        :close-on-click-overlay="false"
+        teleport="body"
+    >
+        <div class="add-machine-popup__content">
+            <div class="form-block">
+                <div class="form-block__title">农机类别</div>
+                <el-select
+                    v-model="form.categoryName"
+                    class="form-select"
+                    placeholder="请选择农机类别"
+                    clearable
+                >
+                    <el-option
+                        v-for="item in categoryOptions"
+                        :key="item"
+                        :label="item"
+                        :value="item"
+                    />
+                </el-select>
+            </div>
+
+            <div class="form-block">
+                <div class="form-block__title">农机名称</div>
+                <el-input v-model="form.name" placeholder="请输入农机名称" />
+            </div>
+
+            <div class="add-machine-popup__confirm" @click="handleConfirm">确认区域</div>
+        </div>
+    </popup>
+</template>
+
+<script setup>
+import { computed, reactive, watch } from "vue";
+import { Popup } from "vant";
+import { ElMessage } from "element-plus";
+
+const props = defineProps({
+    show: {
+        type: Boolean,
+        default: false,
+    },
+    categoryOptions: {
+        type: Array,
+        default: () => [],
+    },
+});
+
+const emit = defineEmits(["update:show", "confirm"]);
+
+const form = reactive({
+    categoryName: "",
+    name: "",
+});
+
+const showValue = computed({
+    get: () => props.show,
+    set: (value) => emit("update:show", value),
+});
+
+watch(
+    () => props.show,
+    (val) => {
+        if (val) {
+            form.categoryName = "";
+            form.name = "";
+        }
+    }
+);
+
+const handleConfirm = () => {
+    const categoryName = String(form.categoryName || "").trim();
+    const name = String(form.name || "").trim();
+    if (!categoryName) {
+        ElMessage.warning("请选择农机类别");
+        return;
+    }
+    if (!name) {
+        ElMessage.warning("请输入农机名称");
+        return;
+    }
+    emit("confirm", { categoryName, name });
+    emit("update:show", false);
+};
+</script>
+
+<style scoped lang="scss">
+.add-machine-popup {
+    width: 330px;
+
+    :deep(.van-popup__close-icon) {
+        color: #333;
+        font-size: 18px;
+    }
+
+    &__content {
+        padding: 24px 16px 20px;
+        background: linear-gradient(360deg, #ffffff 74.2%, #d1ebff 100%);
+        border-radius: 16px;
+        box-sizing: border-box;
+    }
+
+    .form-block {
+        & + .form-block {
+            margin-top: 16px;
+        }
+
+        &__title {
+            margin-bottom: 10px;
+            font-size: 16px;
+            font-weight: 500;
+            color: #000;
+        }
+    }
+
+    .form-select {
+        width: 100%;
+    }
+
+    :deep(.el-select__wrapper),
+    :deep(.el-input__wrapper) {
+        min-height: 40px;
+        border-radius: 4px;
+        box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1) inset;
+        background: #fff;
+    }
+
+    &__confirm {
+        margin-top: 24px;
+        padding: 10px;
+        border-radius: 25px;
+        background: #2199f8;
+        color: #fff;
+        font-size: 16px;
+        text-align: center;
+    }
+}
+</style>

+ 39 - 17
src/views/old_mini/entry_information/components/selectCategory.vue

@@ -2,7 +2,7 @@
     <div class="select-category">
         <div class="select-category__content">
             <div class="page-header">
-                <div class="page-title">请选择您的种植品类</div>
+                <div class="page-title">请选择您的种植品类<span class="page-title-tip">(单选)</span></div>
                 <div class="page-subtitle">完善档案,精准匹配农机服务与农情预警</div>
             </div>
 
@@ -135,22 +135,34 @@ const mapHierarchyToGroups = (list) => {
         .filter((group) => group.name && group.items.length);
 };
 
-const getSelectedIdSet = () => {
+const getSelectedId = () => {
     try {
         const raw = sessionStorage.getItem(SESSION_KEY);
-        if (!raw) return new Set();
-        return new Set(JSON.parse(raw).map((item) => String(item.id)));
+        if (!raw) return null;
+        const list = JSON.parse(raw);
+        if (!Array.isArray(list) || !list.length) return null;
+        return String(list[0].id);
     } catch {
-        return new Set();
+        return null;
     }
 };
 
+const clearAllSelection = () => {
+    majorTabs.value.forEach((tab) => {
+        tab.groups.forEach((group) => {
+            group.items.forEach((item) => {
+                item.selected = false;
+            });
+        });
+    });
+};
+
 const applySelectionToTab = (tab) => {
-    const selectedIds = getSelectedIdSet();
-    if (!selectedIds.size) return;
+    const selectedId = getSelectedId();
+    if (!selectedId) return;
     tab.groups.forEach((group) => {
         group.items.forEach((item) => {
-            item.selected = selectedIds.has(String(item.id));
+            item.selected = String(item.id) === selectedId;
         });
     });
 };
@@ -186,7 +198,15 @@ const handleSearch = () => {
 };
 
 const handleSelect = (item) => {
-    item.selected = !item.selected;
+    const wasSelected = item.selected;
+    clearAllSelection();
+    item.selected = !wasSelected;
+    // 单选:立即覆盖缓存,避免未加载 Tab 的旧多选缓存被合并回来
+    if (item.selected) {
+        sessionStorage.setItem(SESSION_KEY, JSON.stringify(selectedItems.value));
+    } else {
+        sessionStorage.removeItem(SESSION_KEY);
+    }
 };
 
 const getCachedSelectedItems = () => {
@@ -194,7 +214,7 @@ const getCachedSelectedItems = () => {
         const raw = sessionStorage.getItem(SESSION_KEY);
         if (!raw) return [];
         const list = JSON.parse(raw);
-        return Array.isArray(list) ? list : [];
+        return Array.isArray(list) ? list.slice(0, 1) : [];
     } catch {
         return [];
     }
@@ -202,19 +222,19 @@ const getCachedSelectedItems = () => {
 
 const getSubmitSelectedItems = () => {
     const current = selectedItems.value;
+    if (current.length) return current.slice(0, 1);
     const cached = getCachedSelectedItems();
     const loadedKeys = new Set(
         majorTabs.value.filter((tab) => tab.loaded).map((tab) => tab.key)
     );
-    // 已加载 Tab 以当前勾选为准;未加载 Tab 保留缓存里的选择
-    const fromCache = cached.filter((item) => !loadedKeys.has(item.majorKey));
-    return [...current, ...fromCache];
+    // 当前已加载 Tab 无选中时,才用未加载 Tab 的缓存(仅一项)
+    return cached.filter((item) => !loadedKeys.has(item.majorKey)).slice(0, 1);
 };
 
 const handleNext = () => {
     const list = getSubmitSelectedItems();
     if (!list.length) {
-        ElMessage.warning("请至少选择一个种植品类");
+        ElMessage.warning("请选择一个种植品类");
         return;
     }
     sessionStorage.setItem(SESSION_KEY, JSON.stringify(list));
@@ -242,21 +262,23 @@ watch(
     &__content {
         flex: 1;
         overflow-y: auto;
-        padding: 8px 16px 20px;
+        padding: 8px 10px 20px;
     }
 
     .page-header {
-        padding: 8px 4px 16px;
+        padding: 8px 0 20px;
 
         .page-title {
             font-size: 26px;
             color: #005599;
             font-family: "PangMenZhengDao";
             line-height: 36px;
+            .page-title-tip {
+                font-size: 20px;
+            }
         }
 
         .page-subtitle {
-            margin-top: 4px;
             font-size: 14px;
             color: rgba(46, 46, 46, 0.4);
             line-height: 20px;

+ 58 - 3
src/views/old_mini/entry_information/components/selectEquipment.vue

@@ -54,6 +54,22 @@
                 {{ submitting ? "提交中..." : "提交信息" }}
             </div>
         </div>
+
+        <add-machine-popup
+            v-model:show="showAddPopup"
+            :category-options="categoryOptions"
+            @confirm="handleAddConfirm"
+        />
+
+        <tip-popup
+            v-model:show="showSuccessPopup"
+            type="executeSuccess"
+            text="您的信息已上传"
+            text2="请等待诊断报告生成"
+            buttonText="完成"
+            @confirm="handleComplete"
+        />
+
     </div>
 </template>
 
@@ -61,6 +77,8 @@
 import { computed, onMounted, reactive, ref } from "vue";
 import { ElMessage } from "element-plus";
 import { Search } from "@element-plus/icons-vue";
+import addMachinePopup from "./addMachinePopup.vue";
+import tipPopup from "@/components/popup/tipPopup.vue";
 
 const emit = defineEmits(["prev", "confirm"]);
 
@@ -73,10 +91,12 @@ const STEP_KEY = "ENTRY_INFORMATION_STEP";
 
 const loading = ref(false);
 const submitting = ref(false);
+const showAddPopup = ref(false);
 const searchKeyword = ref("");
 const appliedKeyword = ref("");
 const selectedIds = reactive(new Set());
 const equipmentGroups = ref([]);
+let customMachineId = 9000;
 
 const DEFAULT_GROUPS = [
     {
@@ -125,6 +145,10 @@ const displayGroups = computed(() => {
         .filter((group) => group.items.length);
 });
 
+const categoryOptions = computed(() =>
+    equipmentGroups.value.map((group) => group.name).filter(Boolean)
+);
+
 const allEquipmentItems = computed(() =>
     equipmentGroups.value.flatMap((group) => group.items)
 );
@@ -165,7 +189,31 @@ const handleSearch = () => {
 };
 
 const handleAddMachine = () => {
-    ElMessage.info("添加农机功能即将开放");
+    showAddPopup.value = true;
+};
+
+const handleAddConfirm = ({ categoryName, name }) => {
+    let group = equipmentGroups.value.find((item) => item.name === categoryName);
+    if (!group) {
+        group = { name: categoryName, items: [] };
+        equipmentGroups.value.push(group);
+    }
+    const exists = group.items.find((item) => item.name === name);
+    if (exists) {
+        selectedIds.add(String(exists.id));
+        saveSelectionDraft();
+        ElMessage.success("已选中该农机");
+        return;
+    }
+    const newItem = {
+        id: ++customMachineId,
+        name,
+        custom: true,
+    };
+    group.items.push(newItem);
+    selectedIds.add(String(newItem.id));
+    saveSelectionDraft();
+    ElMessage.success("添加成功");
 };
 
 const toggleSelect = (item) => {
@@ -225,6 +273,8 @@ function clearEntrySession() {
     sessionStorage.removeItem(STEP_KEY);
 }
 
+const showSuccessPopup = ref(false);
+
 async function submitEntry(includeEquipment = true) {
     if (submitting.value) return;
     const varietyList = getVarietyDraftList();
@@ -251,9 +301,9 @@ async function submitEntry(includeEquipment = true) {
         // } else {
         //     ElMessage.error(res.msg || "提交失败,请稍后再试");
         // }
-        ElMessage.success("提交成功");
+
+        showSuccessPopup.value = true;
         clearEntrySession();
-        emit("confirm", params);
     } catch (error) {
         console.error("entry submit failed", error);
         ElMessage.error("提交失败");
@@ -262,6 +312,11 @@ async function submitEntry(includeEquipment = true) {
     }
 }
 
+const handleComplete = () => {
+    showSuccessPopup.value = false;
+    emit("confirm");
+};
+
 const handleSkip = () => {
     submitEntry(false);
 };

+ 182 - 269
src/views/old_mini/entry_information/components/selectVariety.vue

@@ -2,28 +2,20 @@
     <div class="select-variety">
         <div class="select-variety__content">
             <div class="page-header">
-                <div class="page-title">请选择您的种植品种</div>
-                <div class="page-subtitle">请确定您的果园设施以及内容</div>
+                <div class="page-title">请选择您的种植品种<span class="page-title-tip">(单选)</span></div>
+                <div class="page-subtitle">精细管理每一块地、每一个品种</div>
             </div>
 
-            <!-- 品类 Tab + 品种选择,照常显示 -->
-            <div class="category-tabs" v-if="categoryTabs.length">
-                <div
-                    v-for="(tab, index) in categoryTabs"
-                    :key="tab.id"
-                    class="category-tab"
-                    :class="{
-                        active: activeCategoryId === tab.id,
-                        first: index % 3 === 0,
-                        last: index === categoryTabs.length - 1,
-                    }"
-                    @click="activeCategoryId = tab.id"
-                >
-                    <span>{{ tab.name }}</span>
-                </div>
-            </div>
 
             <div class="variety-card" v-loading="varietyLoading">
+                <!-- 单品类标题 -->
+                <div class="category-title" v-if="currentCategory?.name">
+                    <span class="category-title__line"></span>
+                    <span class="category-title__diamond"></span>
+                    <span class="category-title__text">品类 - {{ currentCategory.name }}</span>
+                    <span class="category-title__diamond"></span>
+                    <span class="category-title__line right-line"></span>
+                </div>
                 <el-input v-model="searchKeyword" class="search-bar" placeholder="请输入品种" @keyup.enter="handleSearch">
                     <template #prefix>
                         <el-icon class="search-icon">
@@ -50,28 +42,37 @@
                 </div>
             </div>
 
-            <!-- 默认表单卡:始终在已选卡片上方 -->
-            <div class="form-card form-card--default">
+            <!-- 默认表单卡(单选,始终一张) -->
+            <div v-if="currentSelected" :key="currentSelected.uid" class="form-card"
+                :class="hasVariety ? 'selected-card' : 'form-card--default'">
                 <div class="form-card__header">
                     <img class="title-icon" src="@/assets/img/home/label-icon.png" alt="" />
-                    <el-select v-model="defaultForm.varietyId" class="variety-select" placeholder="选择品种"
+                    <el-select
+                        v-model="currentSelected.id"
+                        class="variety-select"
+                        placeholder="选择品种"
                         popper-class="variety-select-popper"
-                        @change="handleDefaultVarietyChange">
+                        @change="handleFormVarietyChange"
+                    >
                         <el-option
-                            v-for="item in currentVarieties"
-                            :key="item.id"
-                            :label="item.name"
-                            :value="item.id"
-                            :class="{ 'is-variety-selected': isSelected(item.id) }"
+                            v-for="opt in currentVarieties"
+                            :key="opt.id"
+                            :label="opt.name"
+                            :value="opt.id"
                         />
                     </el-select>
+                    <span v-if="hasVariety" class="delete-btn" @click="clearVariety">删除</span>
                 </div>
 
-                <div class="form-card__body form-card__body--locked" @click="tipSelectVariety">
+                <div
+                    class="form-card__body"
+                    :class="{ 'form-card__body--locked': !hasVariety }"
+                    @click="onFormBodyClick"
+                >
                     <div class="form-row">
                         <span class="form-label">当下物候期</span>
                         <el-select
-                            v-model="defaultForm.phenologyId"
+                            v-model="currentSelected.phenologyId"
                             class="form-select"
                             placeholder="选择物候期"
                             placement="bottom-end"
@@ -87,9 +88,11 @@
                     </div>
 
                     <div class="form-row">
-                        <span class="form-label muted">{{ currentStartingPointQuestion }}</span>
+                        <span class="form-label" :class="{ muted: !hasVariety }">
+                            {{ currentStartingPointQuestion }}
+                        </span>
                         <el-date-picker
-                            v-model="defaultForm.startTime"
+                            v-model="currentSelected.startTime"
                             class="form-picker"
                             type="date"
                             placeholder="请选择时间"
@@ -97,89 +100,28 @@
                             value-format="YYYY-MM-DD"
                             :clearable="false"
                             style="width: 130px"
-                            readonly
+                            :readonly="!hasVariety"
                         />
                     </div>
 
                     <div class="form-row">
                         <span class="form-label">种植亩数</span>
                         <el-input
-                            v-model.number="defaultForm.area"
+                            v-model="currentSelected.area"
                             class="form-input"
                             placeholder="请输入亩数"
                             type="number"
+                            :readonly="!hasVariety"
                         />
                     </div>
 
                     <div class="form-row location-row">
                         <span class="form-label">选择种植点位</span>
-                        <div class="map" ref="defaultMapRef"></div>
-                    </div>
-                </div>
-            </div>
-
-            <!-- 已选品种详情卡:只显示当前品类 -->
-            <div v-for="item in visibleSelectedList" :key="item.uid" class="form-card selected-card">
-                <div class="form-card__header">
-                    <img class="title-icon" src="@/assets/img/home/label-icon.png" alt="" />
-                    <el-select v-model="item.id" class="variety-select" placeholder="选择品种"
-                        popper-class="variety-select-popper"
-                        @change="(val) => handleSelectedVarietyChange(item.uid, val)">
-                        <el-option
-                            v-for="opt in getVarietiesByCategory(item.categoryId)"
-                            :key="opt.id"
-                            :label="opt.name"
-                            :value="opt.id"
-                            :class="{ 'is-variety-selected': isSelected(opt.id) }"
-                        />
-                    </el-select>
-                    <span class="delete-btn" @click="removeVariety(item.uid)">删除</span>
-                </div>
-
-                <div class="form-card__body">
-                    <div class="form-row">
-                        <span class="form-label">当下物候期</span>
-                        <el-select
-                            v-model="item.phenologyId"
-                            class="form-select"
-                            placeholder="选择物候期"
-                            placement="bottom-end"
-                            popper-class="variety-select-popper"
-                        >
-                            <el-option
-                                v-for="opt in getPhenologyOptions(item.categoryId)"
-                                :key="opt.id"
-                                :label="opt.name"
-                                :value="opt.id"
-                            />
-                        </el-select>
-                    </div>
-
-                    <div class="form-row">
-                        <span class="form-label">{{ getStartingPointQuestion(item.categoryId) }}</span>
-                        <el-date-picker
-                            v-model="item.startTime"
-                            class="form-picker"
-                            type="date"
-                            placeholder="请选择时间"
-                            format="YYYY-MM-DD"
-                            value-format="YYYY-MM-DD"
-                            :clearable="false"
-                            style="width: 130px"
-                        />
-                    </div>
-
-                    <div class="form-row">
-                        <span class="form-label">种植亩数</span>
-                        <el-input v-model="item.area" class="form-input" placeholder="请输入亩数" type="number" />
-                    </div>
-
-                    <div class="form-row location-row">
-                        <span class="form-label">选择种植点位</span>
                         <div
-                            class="map map--clickable"
-                            :ref="(el) => setItemMapRef(item.uid, el)"
-                            @click="goSelectItemLocation(item)"
+                            class="map"
+                            :class="{ 'map--clickable': hasVariety }"
+                            :ref="(el) => setItemMapRef(currentSelected.uid, el)"
+                            @click="goSelectItemLocation(currentSelected)"
                         ></div>
                     </div>
                 </div>
@@ -194,7 +136,7 @@
 </template>
 
 <script setup>
-import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
+import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue";
 import { ElMessage } from "element-plus";
 import { Search } from "@element-plus/icons-vue";
 import { useRouter } from "vue-router";
@@ -205,7 +147,7 @@ const emit = defineEmits(["prev", "next"]);
 const router = useRouter();
 const store = useStore();
 
-const DEFAULT_VISIBLE_COUNT = 8;
+const DEFAULT_VISIBLE_COUNT = 12;
 const DEFAULT_POINT = "POINT(113.6142086995688 23.585836479509055)";
 const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
 const EDIT_UID_KEY = "ENTRY_VARIETY_EDIT_UID";
@@ -214,8 +156,6 @@ const CATEGORY_SESSION_KEY = "ENTRY_SELECTED_CATEGORY";
 const MAP_PADDING = [6, 6, 6, 6];
 let selectedUid = 0;
 
-const defaultMapRef = ref(null);
-let defaultPreviewMap = null;
 const itemPreviewMaps = new Map();
 
 function getDefaultPoint() {
@@ -281,15 +221,6 @@ function setItemMapRef(uid, el) {
     });
 }
 
-function initDefaultMap() {
-    if (!defaultMapRef.value) return;
-    if (defaultPreviewMap) {
-        nextTick(() => defaultPreviewMap.kmap?.map?.updateSize?.());
-        return;
-    }
-    defaultPreviewMap = createPreviewMap(defaultMapRef.value, getDefaultPoint(), false);
-}
-
 function syncItemLocationFromSession() {
     const uid = sessionStorage.getItem(EDIT_UID_KEY);
     if (!uid) return;
@@ -334,8 +265,9 @@ function restoreSelectedListDraft() {
         if (typeof draft?.selectedUid === "number") {
             selectedUid = draft.selectedUid;
         }
-        if (Array.isArray(draft?.list)) {
-            selectedList.value = draft.list;
+        if (Array.isArray(draft?.list) && draft.list.length) {
+            // 单选:只保留一项
+            selectedList.value = draft.list.slice(0, 1);
         }
     } catch {
         // ignore
@@ -343,6 +275,10 @@ function restoreSelectedListDraft() {
 }
 
 const goSelectItemLocation = (item) => {
+    if (!item?.id) {
+        ElMessage.warning("请选择品种");
+        return;
+    }
     sessionStorage.setItem(EDIT_UID_KEY, item.uid);
     sessionStorage.setItem("ENTRY_INFORMATION_STEP", "3");
     saveSelectedListDraft();
@@ -358,8 +294,8 @@ const goSelectItemLocation = (item) => {
 onMounted(async () => {
     initCategoryTabsFromSession();
     restoreSelectedListDraft();
+    ensureDefaultForm();
     syncItemLocationFromSession();
-    nextTick(() => initDefaultMap());
     await Promise.all(
         categoryTabs.value.map((tab) => fetchVarietiesByCrop(tab.id))
     );
@@ -368,9 +304,9 @@ onMounted(async () => {
 onActivated(async () => {
     initCategoryTabsFromSession();
     restoreSelectedListDraft();
+    ensureDefaultForm();
     syncItemLocationFromSession();
     nextTick(() => {
-        initDefaultMap();
         itemPreviewMaps.forEach((map) => map.kmap?.map?.updateSize?.());
     });
     await Promise.all(
@@ -379,8 +315,6 @@ onActivated(async () => {
 });
 
 onBeforeUnmount(() => {
-    destroyPreviewMap(defaultPreviewMap);
-    defaultPreviewMap = null;
     itemPreviewMaps.forEach((map) => destroyPreviewMap(map));
     itemPreviewMaps.clear();
 });
@@ -452,13 +386,6 @@ const searchKeyword = ref("");
 const appliedKeyword = ref("");
 const gridExpanded = ref(false);
 const selectedList = ref([]);
-const defaultForm = reactive({
-    varietyId: "",
-    varietyName: "",
-    phenologyId: "",
-    startTime: "",
-    area: "",
-});
 
 const fetchVarietiesByCrop = async (cropId, force = false) => {
     if (cropId == null || cropId === "") return;
@@ -526,12 +453,6 @@ const getStartingPointQuestion = (categoryId) => {
     return tab?.startingPointQuestion || "起种点问题";
 };
 
-const currentPhenologyOptions = computed(() => getPhenologyOptions(activeCategoryId.value));
-
-const currentStartingPointQuestion = computed(() =>
-    getStartingPointQuestion(activeCategoryId.value)
-);
-
 const currentVarieties = computed(() => currentCategory.value?.varieties || []);
 
 const getVarietiesByCategory = (categoryId) => {
@@ -551,101 +472,112 @@ const visibleVarieties = computed(() => {
     return filteredVarieties.value.slice(0, DEFAULT_VISIBLE_COUNT);
 });
 
-const visibleSelectedList = computed(() =>
-    selectedList.value.filter(
-        (item) => String(item.categoryId) === String(activeCategoryId.value)
-    )
+const createEmptyForm = (category) => ({
+    uid: `sel-${++selectedUid}`,
+    id: "",
+    name: "",
+    categoryId: category?.id ?? null,
+    categoryName: category?.name ?? "",
+    phenologyId: "",
+    startTime: "",
+    area: "",
+    location: "",
+});
+
+/** 始终保留一张可编辑的默认表单卡 */
+const ensureDefaultForm = () => {
+    const category = currentCategory.value;
+    if (!category) {
+        selectedList.value = [];
+        return;
+    }
+    if (!selectedList.value.length) {
+        selectedList.value = [createEmptyForm(category)];
+        return;
+    }
+    const item = selectedList.value[0];
+    item.categoryId = category.id;
+    item.categoryName = category.name;
+};
+
+/** 单选当前表单 */
+const currentSelected = computed(() => selectedList.value[0] || null);
+const hasVariety = computed(() => !!currentSelected.value?.id);
+const currentPhenologyOptions = computed(() =>
+    getPhenologyOptions(currentSelected.value?.categoryId || activeCategoryId.value)
+);
+const currentStartingPointQuestion = computed(() =>
+    getStartingPointQuestion(currentSelected.value?.categoryId || activeCategoryId.value)
 );
 
 watch(activeCategoryId, (id) => {
     searchKeyword.value = "";
     appliedKeyword.value = "";
     gridExpanded.value = false;
-    resetDefaultForm();
+    ensureDefaultForm();
     if (id != null) {
         fetchVarietiesByCrop(id);
     }
 });
 
 const isSelected = (id) =>
-    selectedList.value.some((item) => String(item.id) === String(id));
+    !!id && selectedList.value.some((item) => String(item.id) === String(id));
 
 const handleSearch = () => {
     appliedKeyword.value = searchKeyword.value;
     gridExpanded.value = false;
 };
 
-const createSelectedItem = (variety, category) => ({
-    uid: `sel-${++selectedUid}`,
-    id: variety.id,
-    name: variety.name,
-    categoryId: category.id,
-    categoryName: category.name,
-    phenologyId: "",
-    startTime: "",
-    area: "",
-    location: "",
-});
-
-const resetDefaultForm = () => {
-    defaultForm.varietyId = "";
-    defaultForm.varietyName = "";
-    defaultForm.phenologyId = "";
-    defaultForm.startTime = "";
-    defaultForm.area = "";
-};
-
-const tipSelectVariety = () => {
-    ElMessage.warning("请选择品种");
+const applyVarietyToForm = (variety) => {
+    ensureDefaultForm();
+    const item = selectedList.value[0];
+    if (!item || !variety || !currentCategory.value) return;
+    item.id = variety.id;
+    item.name = variety.name;
+    item.categoryId = currentCategory.value.id;
+    item.categoryName = currentCategory.value.name;
+    item.phenologyId = "";
+    saveSelectedListDraft();
 };
 
-/** 顶部「选择品种」:选中后新增下方卡片,顶部表单重置为空 */
-const handleDefaultVarietyChange = (varietyId) => {
-    if (!varietyId) return;
-    const found = currentVarieties.value.find((item) => String(item.id) === String(varietyId));
-    if (!found) {
-        resetDefaultForm();
+const handleFormVarietyChange = (varietyId) => {
+    if (!varietyId) {
+        clearVariety();
         return;
     }
-    if (!isSelected(found.id)) {
-        selectedList.value.unshift(createSelectedItem(found, currentCategory.value));
-        saveSelectedListDraft();
-    }
-    nextTick(() => {
-        resetDefaultForm();
-    });
+    const found = currentVarieties.value.find((item) => String(item.id) === String(varietyId));
+    if (!found) return;
+    applyVarietyToForm(found);
 };
 
-const handleSelectedVarietyChange = (uid, varietyId) => {
-    const item = selectedList.value.find((i) => i.uid === uid);
+const clearVariety = () => {
+    ensureDefaultForm();
+    const item = selectedList.value[0];
     if (!item) return;
-    const found = getVarietiesByCategory(item.categoryId).find(
-        (opt) => String(opt.id) === String(varietyId)
-    );
-    if (!found) return;
-    item.id = found.id;
-    item.name = found.name;
+    item.id = "";
+    item.name = "";
     item.phenologyId = "";
+    item.startTime = "";
+    item.area = "";
+    item.location = "";
+    nextTick(() => updateItemMap(item.uid, getDefaultPoint(), false));
     saveSelectedListDraft();
 };
 
-const toggleVariety = (variety) => {
-    const index = selectedList.value.findIndex((item) => String(item.id) === String(variety.id));
-    if (index > -1) {
-        selectedList.value.splice(index, 1);
-        saveSelectedListDraft();
-        return;
+const onFormBodyClick = () => {
+    if (!hasVariety.value) {
+        ElMessage.warning("请选择品种");
     }
-    selectedList.value.unshift(createSelectedItem(variety, currentCategory.value));
-    saveSelectedListDraft();
 };
 
-const removeVariety = (uid) => {
-    const index = selectedList.value.findIndex((item) => item.uid === uid);
-    if (index > -1) {
-        selectedList.value.splice(index, 1);
-        saveSelectedListDraft();
+/** 单选:点新品种写入当前表单;再点已选则清空品种 */
+const toggleVariety = (variety) => {
+    const current = selectedList.value[0];
+    if (current && String(current.id) === String(variety.id)) {
+        clearVariety();
+        return;
     }
+    applyVarietyToForm(variety);
 };
 
 const handlePrev = () => {
@@ -674,37 +606,26 @@ const validateSubmitData = () => {
         ElMessage.warning("请先选择种植品类");
         return false;
     }
-    if (!selectedList.value.length) {
-        ElMessage.warning("请至少选择一个品种");
+    const item = selectedList.value[0];
+    if (!item?.id) {
+        ElMessage.warning("请选择一个品种");
         return false;
     }
-    for (const tab of categoryTabs.value) {
-        const hasVariety = selectedList.value.some(
-            (item) => String(item.categoryId) === String(tab.id)
-        );
-        if (!hasVariety) {
-            ElMessage.warning(`请为「${tab.name}」选择品种并填写信息`);
-            activeCategoryId.value = tab.id;
-            return false;
-        }
+    if (!item.phenologyId) {
+        ElMessage.warning(`请选择「${item.name}」的当下物候期`);
+        return false;
     }
-    for (const item of selectedList.value) {
-        if (!item.phenologyId) {
-            ElMessage.warning(`请选择「${item.name}」的当下物候期`);
-            return false;
-        }
-        if (!item.startTime) {
-            ElMessage.warning(`请填写「${item.name}」的起种点时间`);
-            return false;
-        }
-        if (item.area === "" || item.area == null || Number(item.area) <= 0) {
-            ElMessage.warning(`请填写「${item.name}」的种植亩数`);
-            return false;
-        }
-        if (!item.location) {
-            ElMessage.warning(`请选择「${item.name}」的种植点位`);
-            return false;
-        }
+    if (!item.startTime) {
+        ElMessage.warning(`请填写「${item.name}」的起种点时间`);
+        return false;
+    }
+    if (item.area === "" || item.area == null || Number(item.area) <= 0) {
+        ElMessage.warning(`请填写「${item.name}」的种植亩数`);
+        return false;
+    }
+    if (!item.location) {
+        ElMessage.warning(`请选择「${item.name}」的种植点位`);
+        return false;
     }
     const baForm = getBaForm();
     if (!baForm?.name || !baForm?.phone) {
@@ -724,10 +645,11 @@ const handleConfirm = () => {
         return;
     }
     if (!validateSubmitData()) return;
-    selectedList.value.forEach((item) => {
-        const options = getPhenologyOptions(item.categoryId);
-        const found = options.find((opt) => String(opt.id) === String(item.phenologyId));
-        item.phenophase = found?.name || String(item.phenologyId || "");
+    selectedList.value.forEach((row) => {
+        if (!row.id) return;
+        const options = getPhenologyOptions(row.categoryId);
+        const found = options.find((opt) => String(opt.id) === String(row.phenologyId));
+        row.phenophase = found?.name || String(row.phenologyId || "");
     });
     saveSelectedListDraft();
     emit("next");
@@ -756,6 +678,9 @@ const handleConfirm = () => {
                 font-size: 26px;
                 color: #005599;
                 font-family: "PangMenZhengDao";
+                .page-title-tip {
+                    font-size: 20px;
+                }
             }
 
             .page-subtitle {
@@ -763,52 +688,40 @@ const handleConfirm = () => {
             }
         }
 
-        .category-tabs {
+        .category-title {
             display: flex;
-            flex-wrap: wrap;
-            row-gap: 8px;
-            margin-bottom: 10px;
-
-            .category-tab {
-                --arrow: 20px;
-                flex: 0 0 calc((100% + 16px) / 3);
-                height: 30px;
-                display: flex;
-                align-items: center;
-                justify-content: center;
-                color: #4e5969;
-                background: #e8f3ff;
-                cursor: default;
-                pointer-events: none;
-                clip-path: polygon(0 0,
-                        calc(100% - var(--arrow)) 0,
-                        100% 50%,
-                        calc(100% - var(--arrow)) 100%,
-                        0 100%,
-                        var(--arrow) 50%);
-                margin-left: -8px;
-
-                &.first {
-                    margin-left: 0;
-                    clip-path: polygon(0 0,
-                            calc(100% - var(--arrow)) 0,
-                            100% 50%,
-                            calc(100% - var(--arrow)) 100%,
-                            0 100%);
+            align-items: center;
+            justify-content: center;
+            gap: 1px;
+            height: 24px;
+            margin-bottom: 9px;
+            padding: 0 6px;
+            box-sizing: border-box;
+
+            &__line {
+                flex: 1;
+                height: 1px;
+                background: linear-gradient(90deg, rgba(118, 118, 118, 0) 0%, rgba(118, 118, 118, 0.4) 100%);
+                &.right-line {
+                    background: linear-gradient(270deg, rgba(118, 118, 118, 0) 0%, rgba(118, 118, 118, 0.4) 100%);
                 }
+            }
 
-                &.last {
-                    clip-path: polygon(0 0,
-                            100% 0,
-                            100% 100%,
-                            0 100%,
-                            var(--arrow) 50%);
-                }
+            &__diamond {
+                flex-shrink: 0;
+                width: 6px;
+                height: 6px;
+                background: rgba(61, 61, 61, 0.2);
+                transform: rotate(45deg);
+            }
 
-                &.active {
-                    color: #fff;
-                    background: #2199f8;
-                }
+            &__text {
+                flex-shrink: 0;
+                padding: 0 10px;
+                font-size: 16px;
+                color: #3D3D3D;
+                line-height: 24px;
+                white-space: nowrap;
             }
         }
 
@@ -950,11 +863,11 @@ const handleConfirm = () => {
             &--default {
                 .form-card__header {
                     .variety-select {
-
                         :deep(.el-select__selected-item),
                         :deep(.el-select__placeholder) {
-                            color: #1D2129;
+                            color: #1d2129;
                         }
+
                         :deep(.el-select__placeholder.is-transparent) {
                             color: rgba(34, 34, 34, 0.3);
                         }

+ 66 - 0
src/views/old_mini/recordDetails/map/mapManage.js

@@ -5,6 +5,7 @@ import Style from "ol/style/Style";
 import Icon from "ol/style/Icon";
 import Fill from "ol/style/Fill";
 import Stroke from "ol/style/Stroke";
+import Text from "ol/style/Text";
 import { Point, Polygon, MultiPolygon } from "ol/geom";
 import Feature from "ol/Feature";
 import DragPan from "ol/interaction/DragPan";
@@ -93,6 +94,9 @@ class MapManage {
         });
       },
     });
+    this.existingVarietyLayer = new KMap.VectorLayer("existingVarietyLayer", 1080, {
+      style: (feature) => this.createExistingVarietyStyle(feature),
+    });
     this.gridToggleSelect = null;
     this.selectedGridIds = new Set();
     this.terrainGridItems = [];
@@ -112,6 +116,31 @@ class MapManage {
     });
   }
 
+  createExistingVarietyStyle(feature) {
+    const name = String(feature.get("name") || "");
+    const fill = feature.get("fill") || "rgba(76, 175, 80, 0.28)";
+    const stroke = feature.get("stroke") || "#4CAF50";
+    return [
+      new Style({
+        fill: new Fill({ color: fill }),
+        stroke: new Stroke({
+          color: stroke,
+          width: 2,
+        }),
+      }),
+      new Style({
+        text: new Text({
+          text: name,
+          font: "13px sans-serif",
+          fill: new Fill({ color: "#1F1F1F" }),
+          backgroundFill: new Fill({ color: "#ffffff" }),
+          padding: [4, 8, 4, 8],
+          overflow: true,
+        }),
+      }),
+    ];
+  }
+
   createBoundaryStyle() {
     return new Style({
       fill: new Fill({
@@ -152,6 +181,7 @@ class MapManage {
     this.kmap.addXYZLayer(xyz2, { minZoom: 8, maxZoom: 22 }, 2);
     // this.kmap.addLayer(this.clickPointLayer.layer);
     this.kmap.addLayer(this.boundaryLayer.layer);
+    this.kmap.addLayer(this.existingVarietyLayer.layer);
     this.kmap.addLayer(this.gridLayer.layer);
 
     if (this.editable) {
@@ -498,6 +528,42 @@ class MapManage {
   clearAllLayers() {
     this.clearLayer();
     this.clearBoundaryLayer();
+    this.clearExistingVarietyZones();
+  }
+
+  clearExistingVarietyZones() {
+    this.existingVarietyLayer?.source?.clear();
+  }
+
+  /**
+   * 展示已有品种种植范围(只读),不参与勾画结果
+   * @param {{ id?: string|number, name: string, polygon: string, fill?: string, stroke?: string }[]} zones
+   */
+  setExistingVarietyZones(zones = []) {
+    if (!this.kmap || !this.existingVarietyLayer?.source) return;
+    this.clearExistingVarietyZones();
+    const projection = this.kmap.map.getView().getProjection();
+    const list = Array.isArray(zones) ? zones : [];
+    list.forEach((item) => {
+      const wkt = item?.polygon || item?.wkt;
+      if (!wkt) return;
+      try {
+        const geometry = this.wktFormat.readGeometry(String(wkt).trim(), {
+          dataProjection: "EPSG:4326",
+          featureProjection: projection,
+        });
+        const feature = new Feature({ geometry });
+        feature.set("id", item.id);
+        feature.set("name", item.name || "");
+        feature.set("fill", item.fill);
+        feature.set("stroke", item.stroke);
+        feature.set("readonly", true);
+        this.existingVarietyLayer.source.addFeature(feature);
+      } catch (e) {
+        console.warn("[MapManage] existing variety polygon parse failed", e);
+      }
+    });
+    this.existingVarietyLayer.layer.changed();
   }
 
   clearGridLayer() {