Pārlūkot izejas kodu

feat: 选择点位,品类信息

lxf 13 stundas atpakaļ
vecāks
revīzija
2d23f255ef

BIN
src/assets/img/home/checked-bg-top.png


BIN
src/assets/img/map/current-point.png


+ 3 - 0
src/components/pageComponents/locationSearch.vue

@@ -106,6 +106,9 @@ const handleClear = () => {
     .el-select__placeholder{
         color: rgba(255, 255, 255, 0.7);
     }
+    .el-select__input {
+        color: rgba(255, 255, 255, 0.7);
+    }
 }
 .location-search-popper {
     .el-select-dropdown__list {

+ 8 - 1
src/router/globalRoutes.js

@@ -160,7 +160,14 @@ export default [
     {
         path: "/entry_information",
         name: "EntryInformation",
-        meta: { keepAlive: false },
+        meta: { keepAlive: true },
         component: () => import("@/views/old_mini/entry_information/index.vue"),
     },
+    // 选择常驻点位
+    {
+        path: "/entry_select_location",
+        name: "EntrySelectLocation",
+        meta: { keepAlive: false },
+        component: () => import("@/views/old_mini/entry_information/selectLocation.vue"),
+    },
 ];

+ 169 - 23
src/views/old_mini/entry_information/components/baInformation.vue

@@ -38,14 +38,15 @@
                             type="tel"
                         />
                     </el-form-item>
+                    <el-form-item label="常驻点位" prop="location" class="location-form-item">
+                        <div class="location-block" @click="goSelectLocation">
+                            <div class="map-preview">
+                                <div class="map-preview__map" ref="previewMapRef"></div>
+                                <div v-if="!form.location" class="map-preview__mask">点击地图选择常驻点位</div>
+                            </div>
+                        </div>
+                    </el-form-item>
                 </el-form>
-
-                <div class="location-block">
-                    <div class="location-label">常驻点位</div>
-                    <div class="map-placeholder">
-                        <div class="map-placeholder__mask">点击地图选择常驻点位</div>
-                    </div>
-                </div>
             </div>
         </div>
 
@@ -56,14 +57,26 @@
 </template>
 
 <script setup>
-import { reactive, ref } from "vue";
+import { nextTick, onActivated, onBeforeUnmount, onMounted, reactive, ref } from "vue";
+import { useRouter } from "vue-router";
+import { useStore } from "vuex";
+import SelectLocationMap from "../map/selectLocationMap.js";
+
+const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
+const FORM_KEY = "ENTRY_BA_FORM";
+const DEFAULT_POINT = "POINT(113.6142086995688 23.585836479509055)";
 
 const emit = defineEmits(["next"]);
+const router = useRouter();
+const store = useStore();
 
 const formRef = ref(null);
+const previewMapRef = ref(null);
+let previewMap = null;
 const form = reactive({
     name: "",
     phone: "",
+    location: "",
 });
 
 const rules = {
@@ -79,17 +92,122 @@ const rules = {
             trigger: "blur",
         },
     ],
+    location: [{ required: true, message: "请选择常驻点位", trigger: "change" }],
+};
+
+function getDefaultPoint() {
+    return (
+        localStorage.getItem("MINI_USER_LOCATION_POINT") ||
+        store.state.home.miniUserLocationPoint ||
+        DEFAULT_POINT
+    );
+}
+
+function readJson(key) {
+    try {
+        const raw = sessionStorage.getItem(key);
+        return raw ? JSON.parse(raw) : null;
+    } catch {
+        return null;
+    }
+}
+
+function saveFormDraft() {
+    sessionStorage.setItem(
+        FORM_KEY,
+        JSON.stringify({ name: form.name, phone: form.phone })
+    );
+}
+
+function restoreFormDraft() {
+    const draft = readJson(FORM_KEY);
+    if (!draft) return;
+    if (draft.name != null) form.name = draft.name;
+    if (draft.phone != null) form.phone = draft.phone;
+}
+
+function utilWktToCoordinate(point) {
+    const match = String(point).match(/POINT\s*\(([\d.\-]+)\s+([\d.\-]+)\)/i);
+    if (!match) return null;
+    return [Number(match[1]), Number(match[2])];
+}
+
+function initPreviewMap(point, showPoint = !!form.location) {
+    if (!previewMapRef.value || !point) return;
+    const previewPadding = [50, 0, 10, 0];
+    const coordinate = utilWktToCoordinate(point);
+    if (!previewMap) {
+        previewMap = new SelectLocationMap();
+        previewMap.initMap(point, previewMapRef.value, {
+            enableClick: false,
+            padding: previewPadding,
+            showPoint,
+        });
+        return;
+    }
+    if (previewMap.kmap) {
+        if (coordinate) {
+            if (showPoint) {
+                previewMap.showPoint(coordinate);
+            } else {
+                previewMap.hidePoint();
+                previewMap.setMapPosition(coordinate, false);
+            }
+        }
+        return;
+    }
+    previewMap.initMap(point, previewMapRef.value, {
+        enableClick: false,
+        padding: previewPadding,
+        showPoint,
+    });
+}
+
+function syncLocationFromSession() {
+    const saved = readJson(LOCATION_KEY);
+    if (saved?.point) {
+        form.location = saved.point;
+        formRef.value?.clearValidate("location");
+        nextTick(() => initPreviewMap(saved.point, true));
+        return;
+    }
+    nextTick(() => initPreviewMap(getDefaultPoint(), false));
+}
+
+const goSelectLocation = () => {
+    saveFormDraft();
+    const mapCenter = form.location || getDefaultPoint();
+    router.push({
+        path: "/entry_select_location",
+        query: { mapCenter },
+    });
 };
 
 const handleNext = async () => {
     if (!formRef.value) return;
     try {
         await formRef.value.validate();
+        saveFormDraft();
         emit("next", { ...form });
     } catch {
         // 校验未通过
     }
 };
+
+onMounted(() => {
+    restoreFormDraft();
+    syncLocationFromSession();
+});
+
+onActivated(() => {
+    restoreFormDraft();
+    syncLocationFromSession();
+});
+
+onBeforeUnmount(() => {
+    previewMap?.clearLayer();
+    previewMap = null;
+});
 </script>
 
 <style lang="scss" scoped>
@@ -211,18 +329,37 @@ const handleNext = async () => {
                 }
             }
         }
-    }
 
-    .location-block {
-        padding: 10px 0 8px;
+        .location-form-item {
+            display: block;
+
+            :deep(.el-form-item__label) {
+                float: none;
+                display: block;
+                text-align: left;
+                width: auto !important;
+                margin-bottom: 10px;
+                height: auto;
+                line-height: 22px;
+            }
 
-        .location-label {
-            font-size: 15px;
-            color: #1a1a1a;
-            margin-bottom: 10px;
+            :deep(.el-form-item__content) {
+                margin-left: 0 !important;
+                line-height: normal;
+                justify-content: flex-start;
+            }
+
+            :deep(.el-form-item__error) {
+                text-align: left;
+                padding-top: 6px;
+            }
         }
+    }
+
+    .location-block {
+        width: 100%;
 
-        .map-placeholder {
+        .map-preview {
             position: relative;
             width: 100%;
             height: 140px;
@@ -230,30 +367,39 @@ const handleNext = async () => {
             overflow: hidden;
             background: linear-gradient(135deg, #e8eef3 0%, #d4dde6 100%);
 
+            &__map {
+                width: 100%;
+                height: 100%;
+                pointer-events: none;
+            }
+
             &__mask {
                 position: absolute;
                 left: 0;
-                right: 0;
                 top: 0;
-                padding: 8px 12px;
-                background: rgba(0, 0, 0, 0.45);
+                z-index: 2;
+                padding: 0 12px;
+                height: 30px;
+                line-height: 30px;
+                background: rgba(0, 0, 0, 0.4);
                 color: #fff;
-                font-size: 13px;
+                font-size: 14px;
                 text-align: center;
+                width: fit-content;
+                border-radius: 8px 0 8px 0;
             }
         }
     }
 
     .custom-bottom-fixed-btns {
-        background: #FFF;
+        background: #fff;
         box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.4);
 
         .bottom-btn {
-            // flex: 1;
             padding: 0 30px;
             height: 40px;
             line-height: 40px;
-            font-size: 16px;
+            font-size: 14px;
             border-radius: 25px;
         }
 

+ 449 - 12
src/views/old_mini/entry_information/components/selectCategory.vue

@@ -1,43 +1,480 @@
 <template>
     <div class="select-category">
         <div class="select-category__content">
-            <!-- 选择种植品类 -->
-            种植品类
+            <div class="page-header">
+                <div class="page-title">请选择您的种植品类</div>
+                <div class="page-subtitle">完善档案,精准匹配农机服务与农情预警</div>
+            </div>
+
+            <div class="major-tabs">
+                <div
+                    v-for="tab in majorTabs"
+                    :key="tab.key"
+                    class="major-tab"
+                    :class="{ active: activeTabKey === tab.key }"
+                    @click="activeTabKey = tab.key"
+                >
+                    {{ tab.label }}
+                </div>
+            </div>
+
+            <div class="search-bar">
+                <el-icon class="search-icon"><Search /></el-icon>
+                <input
+                    v-model="searchKeyword"
+                    class="search-input"
+                    type="text"
+                    placeholder="请输入品种名称"
+                    @keyup.enter="handleSearch"
+                />
+                <div class="search-btn" @click="handleSearch">搜索</div>
+            </div>
+
+            <div class="category-list">
+                <div
+                    v-for="group in displayGroups"
+                    :key="group.name"
+                    class="category-card"
+                >
+                    <div class="section-title">
+                        <span class="title-icon"></span>
+                        <span>{{ group.name }}</span>
+                    </div>
+                    <div class="tag-group add-tag-group">
+                        <div
+                            v-for="item in group.items"
+                            :key="item.id"
+                            class="tag-item"
+                            :class="{ selected: item.selected }"
+                            @click="handleSelect(item)"
+                        >
+                            <span class="text">{{ item.name }}</span>
+                        </div>
+                    </div>
+                </div>
+                <div v-if="!displayGroups.length" class="empty-tip">暂无匹配品类</div>
+            </div>
         </div>
+
         <div class="custom-bottom-fixed-btns">
             <div class="bottom-btn secondary-btn" @click="emit('prev')">上一步</div>
-            <div class="bottom-btn primary-btn" @click="emit('next')">下一步</div>
+            <div class="bottom-btn primary-btn" @click="handleNext">下一步 (2/3)</div>
         </div>
     </div>
 </template>
 
 <script setup>
+import { computed, onMounted, ref } from "vue";
+import { ElMessage } from "element-plus";
+import { Search } from "@element-plus/icons-vue";
+
+const SESSION_KEY = "ENTRY_SELECTED_CATEGORY";
+
 const emit = defineEmits(["prev", "next"]);
+
+const majorTabs = ref([
+    {
+        key: "fruit",
+        label: "果树类",
+        groups: [
+            {
+                name: "热带亚热带",
+                items: [
+                    { id: "mango", name: "芒果", selected: false },
+                    { id: "litchi", name: "荔枝", selected: false },
+                    { id: "longan", name: "龙眼", selected: false },
+                    { id: "banana", name: "香蕉", selected: false },
+                    { id: "pineapple", name: "菠萝", selected: false },
+                    { id: "coconut", name: "椰子", selected: false },
+                    { id: "papaya", name: "木瓜", selected: false },
+                    { id: "jackfruit", name: "菠萝蜜", selected: false },
+                    { id: "passion", name: "百香果", selected: false },
+                    { id: "durian", name: "榴莲", selected: false },
+                    { id: "rambutan", name: "红毛丹", selected: false },
+                    { id: "waxapple", name: "莲雾", selected: false },
+                ],
+            },
+            {
+                name: "柑果类",
+                items: [
+                    { id: "mandarin", name: "柑橘", selected: false },
+                    { id: "orange", name: "橙子", selected: false },
+                    { id: "pomelo", name: "柚子", selected: false },
+                    { id: "lemon", name: "柠檬", selected: false },
+                    { id: "kumquat", name: "金桔", selected: false },
+                ],
+            },
+            {
+                name: "仁果类",
+                items: [
+                    { id: "apple", name: "苹果", selected: false },
+                    { id: "pear", name: "梨", selected: false },
+                    { id: "hawthorn", name: "山楂", selected: false },
+                ],
+            },
+            {
+                name: "核果类",
+                items: [
+                    { id: "tao", name: "桃", selected: false },
+                    { id: "yt", name: "樱桃", selected: false },
+                    { id: "li", name: "李", selected: false },
+                ],
+            },
+        ],
+    },
+    {
+        key: "field",
+        label: "大田类",
+        groups: [
+            {
+                name: "粮食作物",
+                items: [
+                    { id: "rice", name: "水稻", selected: false },
+                    { id: "wheat", name: "小麦", selected: false },
+                    { id: "corn", name: "玉米", selected: false },
+                    { id: "soybean", name: "大豆", selected: false },
+                ],
+            },
+            {
+                name: "经济作物",
+                items: [
+                    { id: "cotton", name: "棉花", selected: false },
+                    { id: "peanut", name: "花生", selected: false },
+                    { id: "rape", name: "油菜", selected: false },
+                    { id: "sugarcane", name: "甘蔗", selected: false },
+                ],
+            },
+        ],
+    },
+    {
+        key: "vegetable",
+        label: "蔬菜类",
+        groups: [
+            {
+                name: "叶菜类",
+                items: [
+                    { id: "cabbage", name: "白菜", selected: false },
+                    { id: "spinach", name: "菠菜", selected: false },
+                    { id: "lettuce", name: "生菜", selected: false },
+                    { id: "celery", name: "芹菜", selected: false },
+                ],
+            },
+            {
+                name: "瓜果类",
+                items: [
+                    { id: "tomato", name: "番茄", selected: false },
+                    { id: "cucumber", name: "黄瓜", selected: false },
+                    { id: "eggplant", name: "茄子", selected: false },
+                    { id: "pepper", name: "辣椒", selected: false },
+                ],
+            },
+        ],
+    },
+]);
+
+const activeTabKey = ref("fruit");
+const searchKeyword = ref("");
+const appliedKeyword = ref("");
+
+const activeTab = computed(() =>
+    majorTabs.value.find((tab) => tab.key === activeTabKey.value) || majorTabs.value[0]
+);
+
+const displayGroups = computed(() => {
+    const groups = activeTab.value?.groups || [];
+    const keyword = (appliedKeyword.value || "").trim();
+    if (!keyword) return groups;
+    return groups
+        .map((group) => ({
+            ...group,
+            items: group.items.filter((item) => item.name.includes(keyword)),
+        }))
+        .filter((group) => group.items.length);
+});
+
+const selectedItems = computed(() => {
+    const list = [];
+    majorTabs.value.forEach((tab) => {
+        tab.groups.forEach((group) => {
+            group.items.forEach((item) => {
+                if (item.selected) {
+                    list.push({
+                        id: item.id,
+                        name: item.name,
+                        groupName: group.name,
+                        majorKey: tab.key,
+                        majorLabel: tab.label,
+                    });
+                }
+            });
+        });
+    });
+    return list;
+});
+
+const handleSearch = () => {
+    appliedKeyword.value = searchKeyword.value.trim();
+};
+
+const handleSelect = (item) => {
+    item.selected = !item.selected;
+};
+
+const restoreSelection = () => {
+    try {
+        const raw = sessionStorage.getItem(SESSION_KEY);
+        if (!raw) return;
+        const selectedIds = new Set(JSON.parse(raw).map((item) => item.id));
+        majorTabs.value.forEach((tab) => {
+            tab.groups.forEach((group) => {
+                group.items.forEach((item) => {
+                    item.selected = selectedIds.has(item.id);
+                });
+            });
+        });
+    } catch {
+        // ignore
+    }
+};
+
+const handleNext = () => {
+    if (!selectedItems.value.length) {
+        ElMessage.warning("请至少选择一个种植品类");
+        return;
+    }
+    sessionStorage.setItem(SESSION_KEY, JSON.stringify(selectedItems.value));
+    emit("next", selectedItems.value);
+};
+
+onMounted(() => {
+    restoreSelection();
+});
 </script>
 
 <style lang="scss" scoped>
 .select-category {
-    flex: 1;
+    height: calc(100% - 80px);
     display: flex;
     flex-direction: column;
-    overflow: hidden;
-    padding-bottom: 80px;
+    overflow: auto;
 
     &__content {
         flex: 1;
         overflow-y: auto;
+        padding: 8px 16px 20px;
     }
 
-    .custom-bottom-fixed-btns {
-        gap: 12px;
-        background: transparent;
-        box-shadow: none;
+    .page-header {
+        padding: 8px 4px 16px;
 
-        .bottom-btn {
+        .page-title {
+            font-size: 26px;
+            color: #005599;
+            font-family: "PangMenZhengDao";
+            line-height: 36px;
+        }
+
+        .page-subtitle {
+            margin-top: 4px;
+            font-size: 14px;
+            color: rgba(46, 46, 46, 0.4);
+            line-height: 20px;
+        }
+    }
+
+    .major-tabs {
+        display: flex;
+        gap: 10px;
+        margin-bottom: 12px;
+
+        .major-tab {
+            flex: 1;
+            height: 36px;
+            line-height: 36px;
+            text-align: center;
+            border-radius: 8px;
+            font-size: 15px;
+            color: rgba(0, 0, 0, 0.55);
+            background: #fff;
+            cursor: pointer;
+
+            &.active {
+                color: #fff;
+                background: #2199f8;
+                font-weight: 500;
+            }
+        }
+    }
+
+    .search-bar {
+        display: flex;
+        align-items: center;
+        height: 36px;
+        padding: 0 12px;
+        margin-bottom: 10px;
+        background: rgba(255, 255, 255, 0.5);
+        border: 1px solid rgba(33, 153, 248, 0.5);
+        border-radius: 6px;
+
+        .search-icon {
+            color: rgba(0, 0, 0, 0.35);
+            font-size: 16px;
+            margin-right: 6px;
+        }
+
+        .search-input {
             flex: 1;
-            padding: 12px 0;
+            border: none;
+            outline: none;
+            background: transparent;
+            font-size: 14px;
+            color: #333;
+
+            &::placeholder {
+                color: rgba(0, 0, 0, 0.3);
+            }
+        }
+
+        .search-btn {
+            flex-shrink: 0;
+            padding-left: 10px;
+            color: #2199f8;
+            font-size: 14px;
+            cursor: pointer;
+        }
+    }
+
+    .category-list {
+        .category-card {
+            background: #fff;
+            border-radius: 12px;
+            padding: 14px 12px 16px;
+            margin-bottom: 12px;
+        }
+
+        .section-title {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            margin-bottom: 4px;
             font-size: 16px;
+            font-weight: 600;
+            color: #1a1a1a;
+
+            .title-icon {
+                position: relative;
+                width: 14px;
+                height: 14px;
+                flex-shrink: 0;
+
+                &::before,
+                &::after {
+                    content: "";
+                    position: absolute;
+                    width: 10px;
+                    height: 10px;
+                    border-radius: 50%;
+                }
+
+                &::before {
+                    left: 0;
+                    top: 2px;
+                    background: #2199f8;
+                    opacity: 0.85;
+                }
+
+                &::after {
+                    right: 0;
+                    top: 0;
+                    background: #7ec8ff;
+                    opacity: 0.9;
+                }
+            }
+        }
+
+        .empty-tip {
+            padding: 40px 0;
+            text-align: center;
+            color: rgba(0, 0, 0, 0.35);
+            font-size: 14px;
+        }
+    }
+
+    .tag-group {
+        display: grid;
+        grid-template-columns: repeat(4, 1fr);
+        gap: 0 8px;
+        font-size: 14px;
+
+        .tag-item {
+            margin-top: 10px;
+            position: relative;
+            border-radius: 6px;
+            box-sizing: border-box;
+            height: 36px;
+            text-align: center;
+            line-height: 36px;
+            cursor: pointer;
+            transition: all 0.3s;
+            color: #000000;
+            background: rgba(241, 241, 241, 0.12);
+            border: 1px solid #ebebeb;
+
+            .text {
+                display: inline-flex;
+                align-items: center;
+            }
+
+            &.selected {
+                border: 1px solid #2199f8;
+                background: #e8f5ff;
+                color: #2199f8;
+
+                &::after {
+                    content: "";
+                    position: absolute;
+                    z-index: 9;
+                    top: -1px;
+                    right: -1px;
+                    width: 18px;
+                    height: 14px;
+                    background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
+                }
+            }
+        }
+
+        &.add-tag-group {
+            .tag-item {
+                color: #000000;
+                background: rgba(241, 241, 241, 0.12);
+                border: 1px solid #ebebeb;
+
+                &.selected {
+                    border: 1px solid #2199f8;
+                    background: #e8f5ff;
+                    color: #2199f8;
+                }
+            }
+        }
+    }
+
+    .custom-bottom-fixed-btns {
+        display: flex;
+        justify-content: space-between;
+        align-items: baseline;
+        padding: 10px 12px 0 12px;
+        height: 80px;
+        background: #fff;
+        box-sizing: border-box;
+        box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.4);
+
+        .bottom-btn {
+            padding: 0 30px;
+            height: 40px;
+            line-height: 40px;
+            font-size: 14px;
             border-radius: 25px;
+            text-align: center;
         }
 
         .secondary-btn {

+ 2 - 5
src/views/old_mini/entry_information/index.vue

@@ -37,17 +37,14 @@ const handleConfirm = () => {
 
 <style lang="scss" scoped>
 .entry-information-page {
-    min-height: 100vh;
-    // background: #e8f4ff;
-    // background: linear-gradient(180deg, #57B5FF 0%, #BBE1FF 11.5%, #F6F6F6 100%);
-
+    height: 100vh;
     background: linear-gradient(270deg, rgba(218, 195, 255, 0.59) 0%, #E6F2FF 0.01%, #8FC5FE 100%);
     display: flex;
     flex-direction: column;
 }
 
 .entry-information-body {
-    flex: 1;
+    height: 100%;
     display: flex;
     flex-direction: column;
     overflow: hidden;

+ 127 - 0
src/views/old_mini/entry_information/map/selectLocationMap.js

@@ -0,0 +1,127 @@
+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 { boundingExtent } from "ol/extent";
+import { reactive } from "vue";
+
+export let mapLocation = reactive({
+  data: null,
+});
+
+/**
+ * @description 常驻点位选点地图
+ */
+class SelectLocationMap {
+  constructor() {
+    this.clickPointLayer = new KMap.VectorLayer("clickPointLayer", 9999, {
+      style: () => {
+        return new Style({
+          image: new Icon({
+            src: require("@/assets/img/map/current-point.png"),
+            scale: 0.45,
+            anchor: [0.5, 1],
+          }),
+        });
+      },
+    });
+    this._clickKey = null;
+    /** @type {[number, number, number, number]|null} [top, right, bottom, left] */
+    this._padding = null;
+    this._showPoint = true;
+  }
+
+  /**
+   * @param {string} location WKT POINT
+   * @param {HTMLElement} target
+   * @param {{ enableClick?: boolean, padding?: [number, number, number, number], showPoint?: boolean }} [options]
+   */
+  initMap(location, target, options = {}) {
+    const { enableClick = true, padding = null, showPoint = true } = options;
+    this._padding = padding;
+    this._showPoint = showPoint;
+    const level = 16;
+    const coordinate = util.wktCastGeom(location).getFirstCoordinate();
+    this.kmap = new KMap.Map(target, level, coordinate[0], coordinate[1], null, 8, 22);
+    const 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);
+    if (showPoint) {
+      this.setMapPoint(coordinate);
+    }
+    mapLocation.data = coordinate;
+    if (enableClick) {
+      this.addMapSingerClick();
+    }
+    this.fitCenter(coordinate, padding, showPoint);
+  }
+
+  /**
+   * 用 padding 调整点位在视图中的位置
+   * padding: [上, 右, 下, 左],上边距越大,点位越靠下
+   */
+  fitCenter(coordinate, padding = this._padding, showPoint = this._showPoint) {
+    if (!this.kmap || !coordinate) return;
+    const center = [Number(coordinate[0]), Number(coordinate[1])];
+    this.kmap.map?.updateSize?.();
+    if (padding) {
+      const extent = boundingExtent([center]);
+      this.kmap.getView().fit(extent, {
+        padding,
+        maxZoom: 16,
+        duration: 0,
+      });
+    } else {
+      this.kmap.getView().setCenter(center);
+    }
+    if (showPoint !== false) {
+      this.setMapPoint(center);
+    }
+  }
+
+  setMapPosition(center, showPoint = this._showPoint) {
+    if (!this.kmap) return;
+    const nextCenter = [Number(center[0]), Number(center[1])];
+    this._showPoint = showPoint;
+    this.fitCenter(nextCenter, this._padding, showPoint);
+    mapLocation.data = nextCenter;
+  }
+
+  /** 显示点位 */
+  showPoint(coordinate) {
+    this._showPoint = true;
+    if (coordinate) {
+      this.setMapPoint(coordinate);
+      this.fitCenter(coordinate, this._padding, true);
+    }
+  }
+
+  /** 清除点位(保留底图) */
+  hidePoint() {
+    this._showPoint = false;
+    this.clearLayer();
+  }
+
+  setMapPoint(coordinate) {
+    this.clickPointLayer.source.clear();
+    const point = new Feature(new Point(coordinate));
+    this.clickPointLayer.addFeature(point);
+  }
+
+  addMapSingerClick() {
+    const that = this;
+    that._clickKey = that.kmap.on("singleclick", (evt) => {
+      that.setMapPoint(evt.coordinate);
+      mapLocation.data = evt.coordinate;
+    });
+  }
+
+  clearLayer() {
+    this.clickPointLayer?.source?.clear();
+  }
+}
+
+export default SelectLocationMap;

+ 169 - 0
src/views/old_mini/entry_information/selectLocation.vue

@@ -0,0 +1,169 @@
+<template>
+    <div class="select-location">
+        <custom-header name="选择种植点位"></custom-header>
+        <div class="select-location-content">
+            <div class="search-bar">
+                <location-search
+                    class="location-search"
+                    :user-location="userLocation"
+                    @change="handleLocationChange"
+                />
+            </div>
+            <div class="map-container" ref="mapContainer"></div>
+            <div class="locate-btn" @click="handleLocate">
+                <img class="locate-icon" src="@/assets/img/map/map-icon.png" alt="" />
+            </div>
+        </div>
+        <div class="custom-bottom-fixed-btns">
+            <div class="bottom-btn primary-btn" @click="handleSubmit">确认点位</div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { onMounted, onUnmounted, ref } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { useStore } from "vuex";
+import { ElMessage } from "element-plus";
+import customHeader from "@/components/customHeader.vue";
+import locationSearch from "@/components/pageComponents/locationSearch.vue";
+import SelectLocationMap, { mapLocation } from "./map/selectLocationMap.js";
+import { convertPointToArray } from "@/utils/index";
+
+const SESSION_KEY = "ENTRY_RESIDENT_LOCATION";
+const DEFAULT_POINT = "POINT(113.6142086995688 23.585836479509055)";
+
+const router = useRouter();
+const route = useRoute();
+const store = useStore();
+const mapContainer = ref(null);
+const selectLocationMap = new SelectLocationMap();
+
+const userLocation = ref(
+    store.state.home.miniUserLocation || localStorage.getItem("MINI_USER_LOCATION") || "113.61702297075017,23.584863449735067"
+);
+
+function getDefaultPoint() {
+    if (route.query.mapCenter) return route.query.mapCenter;
+    return (
+        localStorage.getItem("MINI_USER_LOCATION_POINT") ||
+        store.state.home.miniUserLocationPoint ||
+        DEFAULT_POINT
+    );
+}
+
+function toPointWkt(coordinate) {
+    const lng = Number(coordinate[0]);
+    const lat = Number(coordinate[1]);
+    return `POINT(${lng} ${lat})`;
+}
+
+onMounted(() => {
+    const point = getDefaultPoint();
+    selectLocationMap.initMap(point, mapContainer.value);
+});
+
+onUnmounted(() => {
+    selectLocationMap.clearLayer();
+});
+
+const handleLocationChange = (payload) => {
+    if (!payload?.coordinateArray) return;
+    selectLocationMap.setMapPosition(payload.coordinateArray);
+};
+
+const handleLocate = () => {
+    const point =
+        localStorage.getItem("MINI_USER_LOCATION_POINT") ||
+        store.state.home.miniUserLocationPoint ||
+        DEFAULT_POINT;
+    const coordinate = convertPointToArray(point).map(Number);
+    selectLocationMap.setMapPosition(coordinate);
+};
+
+const handleSubmit = () => {
+    const coordinate = mapLocation.data;
+    if (!coordinate) {
+        ElMessage.warning("请选择常驻点位");
+        return;
+    }
+    const point = toPointWkt(coordinate);
+    sessionStorage.setItem(
+        SESSION_KEY,
+        JSON.stringify({
+            point,
+            coordinate: [Number(coordinate[0]), Number(coordinate[1])],
+        })
+    );
+    router.back();
+};
+</script>
+
+<style lang="scss" scoped>
+.select-location {
+    width: 100%;
+    height: 100vh;
+    overflow: hidden;
+    background: #fff;
+    display: flex;
+    flex-direction: column;
+
+    .select-location-content {
+        position: relative;
+        flex: 1;
+        overflow: hidden;
+
+        .search-bar {
+            position: absolute;
+            top: 12px;
+            left: 12px;
+            right: 12px;
+            z-index: 2;
+        }
+
+        .map-container {
+            width: 100%;
+            height: 100%;
+        }
+
+        .locate-btn {
+            position: absolute;
+            right: 12px;
+            bottom: 40px;
+            z-index: 2;
+            width: 32px;
+            height: 32px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: #fff;
+            border-radius: 8px;
+            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+
+            .locate-icon {
+                width: 16px;
+                height: 18px;
+            }
+        }
+    }
+
+    .custom-bottom-fixed-btns {
+        position: relative;
+        background: #fff;
+        box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.4);
+
+        .bottom-btn {
+            padding: 0 30px;
+            height: 40px;
+            line-height: 40px;
+            font-size: 14px;
+            border-radius: 25px;
+        }
+
+        .primary-btn {
+            background: #2199f8;
+            color: #fff;
+        }
+    }
+}
+</style>