Sfoglia il codice sorgente

feat;添加邀请农服逻辑

wangsisi 8 ore fa
parent
commit
58ae45e931

+ 1 - 1
src/components/popup/tipPopup.vue

@@ -29,7 +29,7 @@
             <img class="tip-icon success-icon" src="@/assets/img/home/right.png" alt="" />
             <div class="tip-text execute-success-text" :class="{ 'no-bottom': hideBtn && !noticeText }">
                 <div class="execute-success-title">{{ text }}</div>
-                <div class="execute-success-subtitle">{{ text2 }}</div>
+                <div v-if="text2" class="execute-success-subtitle">{{ text2 }}</div>
             </div>
             <div v-if="noticeText" class="tip-notice">{{ noticeText }}</div>
         </template>

+ 16 - 3
src/views/old_mini/entry_information/components/inviteEntryPopup.vue

@@ -11,7 +11,10 @@
             <img class="invite-entry-popup__cover" src="@/assets/img/home/plant.png" alt="" />
             <div class="invite-entry-popup__text">
                 <div class="invite-entry-popup__line">管理员邀请您</div>
-                <div class="invite-entry-popup__line">
+                <div class="invite-entry-popup__line" v-if="inviteType === 'service'">
+                    完善 <span class="highlight">农服信息</span>
+                </div>
+                <div class="invite-entry-popup__line" v-else>
                     录入 <span class="highlight">地块种植信息</span>
                 </div>
             </div>
@@ -25,10 +28,15 @@ import { ref } from "vue";
 import { useRouter } from "vue-router";
 import { Popup } from "vant";
 
+const INVITE_TYPE_KEY = "ENTRY_INVITE_TYPE";
+const STEP_KEY = "ENTRY_INFORMATION_STEP";
+
 const router = useRouter();
 const show = ref(false);
+const inviteType = ref("farmer");
 
-const open = () => {
+const open = (type = "farmer") => {
+    inviteType.value = type === "service" ? "service" : "farmer";
     show.value = true;
 };
 
@@ -38,7 +46,12 @@ const close = () => {
 
 const handleConfirm = () => {
     close();
-    router.push("/entry_information");
+    sessionStorage.setItem(INVITE_TYPE_KEY, inviteType.value);
+    sessionStorage.removeItem(STEP_KEY);
+    router.push({
+        path: "/entry_information",
+        query: { inviteType: inviteType.value },
+    });
 };
 
 defineExpose({ open, close });

+ 666 - 0
src/views/old_mini/entry_information/components/manualFarmingService.vue

@@ -0,0 +1,666 @@
+<template>
+    <div class="manual-farming">
+        <div class="manual-farming__content">
+            <div class="page-header">
+                <div class="page-title">完善人工农事服务</div>
+                <div class="page-subtitle">让农事调度更高效(可多选)</div>
+            </div>
+
+            <div class="toolbar">
+                <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="add-btn" @click="showAddPopup = true">+ 添加农事</div>
+            </div>
+
+            <div class="task-card" v-if="!isFruitView">
+                <div class="tag-group">
+                    <div
+                        v-for="item in displayFieldTasks"
+                        :key="item.id"
+                        class="tag-item"
+                        :class="{ selected: selectedIds.has(String(item.id)) }"
+                        @click="toggleSelect(item)"
+                    >
+                        <span class="text">{{ item.name }}</span>
+                    </div>
+                </div>
+                <div v-if="!displayFieldTasks.length" class="empty-tip">暂无匹配农事</div>
+            </div>
+
+            <div v-else class="fruit-list">
+                <div v-for="group in displayFruitGroups" :key="group.name" class="task-card fruit-card">
+                    <div class="section-title">
+                        <img class="title-icon" src="@/assets/img/home/label-icon.png" alt="" />
+                        <span>{{ group.name }}</span>
+                    </div>
+                    <div class="tag-group">
+                        <div
+                            v-for="item in group.items"
+                            :key="item.id"
+                            class="tag-item"
+                            :class="{ selected: selectedIds.has(String(item.id)) }"
+                            @click="toggleSelect(item)"
+                        >
+                            <span class="text">{{ item.name }}</span>
+                        </div>
+                    </div>
+                </div>
+                <div v-if="!displayFruitGroups.length" class="empty-tip">暂无匹配农事</div>
+            </div>
+        </div>
+
+        <div class="custom-bottom-fixed-btns">
+            <div class="bottom-btn secondary-btn" @click="handlePrevClick">上一步</div>
+            <div class="bottom-btn primary-btn" :class="{ disabled: submitting }" @click="handlePrimaryClick">
+                {{ primaryBtnText }}
+            </div>
+        </div>
+
+        <popup
+            v-model:show="showAddPopup"
+            round
+            closeable
+            class="add-task-popup"
+            :close-on-click-overlay="false"
+            teleport="body"
+        >
+            <div class="add-task-popup__content">
+                <div class="form-block__title">农事名称</div>
+                <el-input v-model="newTaskName" placeholder="请输入农事名称" />
+                <div class="add-task-popup__confirm" @click="handleAddConfirm">确认</div>
+            </div>
+        </popup>
+
+        <tip-popup
+            v-model:show="showSuccessPopup"
+            type="executeSuccess"
+            text="您的信息已提交成功"
+            buttonText="完成"
+            @confirm="handleComplete"
+        />
+    </div>
+</template>
+
+<script setup>
+import { computed, onMounted, reactive, ref } from "vue";
+import { ElMessage } from "element-plus";
+import { Search } from "@element-plus/icons-vue";
+import { Popup } from "vant";
+import tipPopup from "@/components/popup/tipPopup.vue";
+
+const TASK_KEY = "ENTRY_SELECTED_FARM_TASKS";
+const TASK_VIEW_KEY = "ENTRY_MANUAL_VIEW";
+const EQUIPMENT_KEY = "ENTRY_SELECTED_EQUIPMENT";
+const SELECTED_LIST_KEY = "ENTRY_SELECTED_VARIETY_LIST";
+const CATEGORY_SESSION_KEY = "ENTRY_SELECTED_CATEGORY";
+const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
+const EDIT_UID_KEY = "ENTRY_VARIETY_EDIT_UID";
+const STEP_KEY = "ENTRY_INFORMATION_STEP";
+
+const emit = defineEmits(["prev", "next", "confirm"]);
+const props = defineProps({
+    hasField: { type: Boolean, default: false },
+    hasFruit: { type: Boolean, default: false },
+});
+
+const FIELD_TASKS = [
+    { id: 1, name: "人工补苗" },
+    { id: 2, name: "人工授粉" },
+    { id: 3, name: "人工除草" },
+    { id: 4, name: "清沟修渠" },
+    { id: 5, name: "排水晒田" },
+    { id: 6, name: "甘蔗砍收" },
+    { id: 7, name: "棉花采摘" },
+];
+
+const FRUIT_GROUPS = [
+    {
+        name: "树体管理",
+        items: [
+            { id: 201, name: "整形修剪" },
+            { id: 202, name: "嫁接" },
+            { id: 203, name: "环割环剥" },
+            { id: 204, name: "枝梢管理" },
+        ],
+    },
+    {
+        name: "花果管理",
+        items: [
+            { id: 301, name: "疏花疏果" },
+            { id: 302, name: "果实套袋" },
+            { id: 303, name: "人工授粉" },
+            { id: 304, name: "果实增色" },
+        ],
+    },
+    {
+        name: "采收与园地管理",
+        items: [
+            { id: 401, name: "人工采摘" },
+            { id: 402, name: "清园" },
+            { id: 403, name: "清沟修渠" },
+            { id: 404, name: "摇花抖水" },
+            { id: 405, name: "葡萄绑蔓/夏剪" },
+            { id: 406, name: "防寒保暖" },
+        ],
+    },
+    {
+        name: "其他人工服务",
+        items: [
+            { id: 501, name: "补植/定植" },
+            { id: 502, name: "行间中耕松土" },
+        ],
+    },
+];
+
+const searchKeyword = ref("");
+const appliedKeyword = ref("");
+const selectedIds = reactive(new Set());
+const fieldTaskList = ref(FIELD_TASKS.map((item) => ({ ...item })));
+const fruitGroups = ref(FRUIT_GROUPS.map((group) => ({
+    name: group.name,
+    items: group.items.map((item) => ({ ...item })),
+})));
+const showAddPopup = ref(false);
+const newTaskName = ref("");
+const submitting = ref(false);
+const showSuccessPopup = ref(false);
+const currentView = ref("field");
+let customTaskId = 900;
+
+const isFruitView = computed(() => currentView.value === "fruit");
+const needFruitAfterField = computed(() => props.hasField && props.hasFruit);
+const isLastTaskView = computed(() => !props.hasFruit);
+const primaryBtnText = computed(() => {
+    if (submitting.value) return "提交中...";
+    if (isLastTaskView.value) return "提交信息";
+    if (needFruitAfterField.value) {
+        return isFruitView.value ? "下一步 (5/6)" : "下一步 (4/6)";
+    }
+    return "下一步 (3/4)";
+});
+
+const displayFieldTasks = computed(() => {
+    const keyword = (appliedKeyword.value || "").trim();
+    if (!keyword) return fieldTaskList.value;
+    return fieldTaskList.value.filter((item) => item.name.includes(keyword));
+});
+
+const displayFruitGroups = computed(() => {
+    const keyword = (appliedKeyword.value || "").trim();
+    const groups = fruitGroups.value.map((group) => ({
+        ...group,
+        items: keyword ? group.items.filter((item) => item.name.includes(keyword)) : group.items,
+    }));
+    return groups.filter((group) => group.items.length);
+});
+
+const allFruitTasks = computed(() => fruitGroups.value.flatMap((group) => group.items));
+
+function readJson(key) {
+    try {
+        const raw = sessionStorage.getItem(key);
+        return raw ? JSON.parse(raw) : null;
+    } catch {
+        return null;
+    }
+}
+
+function restoreSelection() {
+    const cached = readJson(TASK_KEY);
+    const list = Array.isArray(cached)
+        ? cached
+        : [...(cached?.field || []), ...(cached?.fruit || [])];
+    list.forEach((item) => {
+        if (item?.id == null) return;
+        selectedIds.add(String(item.id));
+        if (item.scope === "fruit" || Number(item.id) >= 200) {
+            const exists = allFruitTasks.value.some((row) => String(row.id) === String(item.id));
+            if (!exists) {
+                fruitGroups.value[fruitGroups.value.length - 1].items.push({
+                    id: item.id,
+                    name: item.name,
+                    custom: true,
+                });
+            }
+            return;
+        }
+        if (!fieldTaskList.value.some((row) => String(row.id) === String(item.id))) {
+            fieldTaskList.value.push({ id: item.id, name: item.name, custom: true });
+        }
+    });
+}
+
+function saveSelectionDraft() {
+    const field = fieldTaskList.value
+        .filter((item) => selectedIds.has(String(item.id)))
+        .map((item) => ({ ...item, scope: "field" }));
+    const fruit = allFruitTasks.value
+        .filter((item) => selectedIds.has(String(item.id)))
+        .map((item) => ({ ...item, scope: "fruit" }));
+    sessionStorage.setItem(TASK_KEY, JSON.stringify({ field, fruit }));
+}
+
+const handleSearch = () => {
+    appliedKeyword.value = searchKeyword.value.trim();
+};
+
+const toggleSelect = (item) => {
+    const id = String(item.id);
+    if (selectedIds.has(id)) selectedIds.delete(id);
+    else selectedIds.add(id);
+    saveSelectionDraft();
+};
+
+const handleAddConfirm = () => {
+    const name = String(newTaskName.value || "").trim();
+    if (!name) {
+        ElMessage.warning("请输入农事名称");
+        return;
+    }
+    const list = isFruitView.value ? allFruitTasks.value : fieldTaskList.value;
+    const exists = list.find((item) => item.name === name);
+    if (exists) {
+        selectedIds.add(String(exists.id));
+        saveSelectionDraft();
+        ElMessage.success("已选中该农事");
+        showAddPopup.value = false;
+        newTaskName.value = "";
+        return;
+    }
+    const newItem = { id: ++customTaskId, name, custom: true };
+    if (isFruitView.value) {
+        fruitGroups.value[fruitGroups.value.length - 1].items.push(newItem);
+    } else {
+        fieldTaskList.value.push(newItem);
+    }
+    selectedIds.add(String(newItem.id));
+    saveSelectionDraft();
+    showAddPopup.value = false;
+    newTaskName.value = "";
+    ElMessage.success("添加成功");
+};
+
+function getVarietyDraftList() {
+    const draft = readJson(SELECTED_LIST_KEY);
+    if (Array.isArray(draft)) return draft;
+    if (Array.isArray(draft?.list)) return draft.list;
+    return [];
+}
+
+function getPhenophaseLabel(item) {
+    return item.phenophase || item.phenologyName || String(item.phenologyId || "");
+}
+
+function buildPlotPayload() {
+    const baForm = readJson("ENTRY_BA_FORM") || {};
+    const varietyList = getVarietyDraftList();
+    const equipmentCache = readJson(EQUIPMENT_KEY);
+    const equipmentList = Array.isArray(equipmentCache)
+        ? equipmentCache
+        : [...(equipmentCache?.field || []), ...(equipmentCache?.fruit || [])];
+    const fieldSelected = fieldTaskList.value.filter((item) => selectedIds.has(String(item.id)));
+    const fruitSelected = allFruitTasks.value.filter((item) => selectedIds.has(String(item.id)));
+    const taskSelected = [...fieldSelected, ...fruitSelected];
+    return {
+        user_name: baForm.name,
+        tel: baForm.phone,
+        team_name: baForm.teamName || "",
+        team_type: baForm.teamType || "",
+        invite_type: baForm.inviteType || "service",
+        crops: varietyList.map((item) => ({
+            crop_type: item.categoryName,
+            crop_id: Number(item.categoryId),
+            variety_list: [Number(item.id)],
+            phenophase: getPhenophaseLabel(item),
+            start_time: item.startTime,
+            plant_area: Number(item.area),
+            point: item.location,
+        })),
+        farm_machines: equipmentList.map((item) => ({ id: item.id, name: item.name })),
+        farm_tasks: taskSelected.map((item) => ({ id: item.id, name: item.name })),
+        field_tasks: fieldSelected.map((item) => ({ id: item.id, name: item.name })),
+        fruit_tasks: fruitSelected.map((item) => ({ id: item.id, name: item.name })),
+    };
+}
+
+function clearEntrySession() {
+    sessionStorage.removeItem(SELECTED_LIST_KEY);
+    sessionStorage.removeItem(CATEGORY_SESSION_KEY);
+    sessionStorage.removeItem("ENTRY_BA_FORM");
+    sessionStorage.removeItem(LOCATION_KEY);
+    sessionStorage.removeItem(EDIT_UID_KEY);
+    sessionStorage.removeItem(EQUIPMENT_KEY);
+    sessionStorage.removeItem(TASK_KEY);
+    sessionStorage.removeItem(TASK_VIEW_KEY);
+    sessionStorage.removeItem(STEP_KEY);
+    sessionStorage.removeItem("ENTRY_INVITE_TYPE");
+}
+
+const handleSubmit = () => {
+    if (submitting.value) return;
+    const baForm = readJson("ENTRY_BA_FORM");
+    if (!baForm?.name || !baForm?.phone) {
+        ElMessage.warning("请先完善个人信息");
+        return;
+    }
+    submitting.value = true;
+    try {
+        const params = buildPlotPayload();
+        console.log("entry submit params", params);
+        showSuccessPopup.value = true;
+        clearEntrySession();
+    } catch (error) {
+        console.error("entry submit failed", error);
+        ElMessage.error("提交失败");
+    } finally {
+        submitting.value = false;
+    }
+};
+
+const handleComplete = () => {
+    showSuccessPopup.value = false;
+    emit("confirm");
+};
+
+const handlePrevClick = () => {
+    if (isFruitView.value && props.hasField) {
+        currentView.value = "field";
+        sessionStorage.setItem(TASK_VIEW_KEY, "field");
+        searchKeyword.value = "";
+        appliedKeyword.value = "";
+        return;
+    }
+    sessionStorage.removeItem(TASK_VIEW_KEY);
+    emit("prev");
+};
+
+const handlePrimaryClick = () => {
+    saveSelectionDraft();
+    if (!isLastTaskView.value) {
+        if (needFruitAfterField.value && !isFruitView.value) {
+            currentView.value = "fruit";
+            sessionStorage.setItem(TASK_VIEW_KEY, "fruit");
+            searchKeyword.value = "";
+            appliedKeyword.value = "";
+            return;
+        }
+        sessionStorage.setItem(TASK_VIEW_KEY, currentView.value);
+        emit("next");
+        return;
+    }
+    handleSubmit();
+};
+
+onMounted(() => {
+    const savedView = sessionStorage.getItem(TASK_VIEW_KEY);
+    if (savedView === "fruit" && props.hasFruit) {
+        currentView.value = "fruit";
+    } else {
+        currentView.value = props.hasField ? "field" : "fruit";
+    }
+    restoreSelection();
+});
+</script>
+
+<style lang="scss" scoped>
+.manual-farming {
+    flex: 1;
+    min-height: 0;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+
+    &__content {
+        flex: 1;
+        min-height: 0;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+        padding: 8px 16px 80px;
+    }
+
+    .page-header {
+        padding: 8px 4px 16px;
+
+        .page-title {
+            font-size: 26px;
+            color: #005599;
+            font-family: "PangMenZhengDao";
+            line-height: 36px;
+        }
+
+        .page-subtitle {
+            margin-top: 4px;
+            font-size: 16px;
+            font-weight: 600;
+            color: #005599;
+            line-height: 22px;
+        }
+    }
+
+    .toolbar {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+        margin-bottom: 12px;
+    }
+
+    .search-bar {
+        flex: 1;
+        min-width: 0;
+        display: flex;
+        align-items: center;
+        height: 36px;
+        padding: 0 12px;
+        background: rgba(255, 255, 255, 0.5);
+        border: 1px solid rgba(33, 153, 248, 0.5);
+        border-radius: 6px;
+        box-sizing: border-box;
+
+        .search-icon {
+            color: rgba(0, 0, 0, 0.35);
+            font-size: 16px;
+            margin-right: 6px;
+            flex-shrink: 0;
+        }
+
+        .search-input {
+            flex: 1;
+            min-width: 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;
+        }
+    }
+
+    .add-btn {
+        flex-shrink: 0;
+        height: 36px;
+        padding: 0 12px;
+        border-radius: 6px;
+        background: #2199f8;
+        color: #fff;
+        font-size: 14px;
+        line-height: 36px;
+        white-space: nowrap;
+    }
+
+    .task-card {
+        background: #fff;
+        border-radius: 12px;
+        padding: 14px 12px 16px;
+    }
+
+    .fruit-card {
+        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 {
+            width: 15px;
+            height: 15px;
+            object-fit: contain;
+            flex-shrink: 0;
+        }
+    }
+
+    .tag-group {
+        display: grid;
+        grid-template-columns: repeat(3, 1fr);
+        gap: 0 8px;
+    }
+
+    .tag-item {
+        margin-top: 10px;
+        position: relative;
+        border-radius: 6px;
+        box-sizing: border-box;
+        min-height: 36px;
+        padding: 6px 4px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        text-align: center;
+        color: #000;
+        background: #f3f4f6;
+        border: 1px solid transparent;
+
+        .text {
+            font-size: 13px;
+            line-height: 16px;
+            padding: 0 4px;
+        }
+
+        &.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;
+            }
+        }
+    }
+
+    .empty-tip {
+        padding: 40px 0;
+        text-align: center;
+        color: rgba(0, 0, 0, 0.35);
+        font-size: 14px;
+    }
+
+    .custom-bottom-fixed-btns {
+        display: flex;
+        justify-content: space-between;
+        align-items: baseline;
+        gap: 10px;
+        padding: 12px 12px 0;
+        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 {
+            color: #2199f8;
+            background: #fff;
+            border: 1px solid #2199f8;
+        }
+
+        .primary-btn {
+            background: #2199f8;
+            color: #fff;
+
+            &.disabled {
+                opacity: 0.6;
+            }
+        }
+    }
+}
+</style>
+
+<style lang="scss">
+.add-task-popup {
+    width: 330px;
+
+    .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__title {
+        margin-bottom: 10px;
+        font-size: 16px;
+        font-weight: 500;
+        color: #000;
+    }
+
+    .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>

+ 78 - 58
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">请选择您的种植品类<span class="page-title-tip">(单选)</span></div>
+                <div class="page-title">请选择您的种植品类<span class="page-title-tip">({{ multiple ? "多选" : "单选" }})</span></div>
                 <div class="page-subtitle">完善档案,精准匹配农机服务与农情预警</div>
             </div>
 
@@ -37,7 +37,7 @@
                     class="category-card"
                 >
                     <div class="section-title">
-                        <span class="title-icon"></span>
+                        <img class="title-icon" src="@/assets/img/home/label-icon.png" alt="" />
                         <span>{{ group.name }}</span>
                     </div>
                     <div class="tag-group add-tag-group">
@@ -58,7 +58,7 @@
 
         <div class="custom-bottom-fixed-btns">
             <div class="bottom-btn secondary-btn" @click="emit('prev')">上一步</div>
-            <div class="bottom-btn primary-btn" @click="handleNext">下一步 (2/4)</div>
+            <div class="bottom-btn primary-btn" @click="handleNext">{{ nextBtnText }}</div>
         </div>
     </div>
 </template>
@@ -70,6 +70,10 @@ import { Search } from "@element-plus/icons-vue";
 
 const SESSION_KEY = "ENTRY_SELECTED_CATEGORY";
 
+const props = defineProps({
+    multiple: { type: Boolean, default: false },
+});
+
 const emit = defineEmits(["prev", "next"]);
 
 /** first_crop 与接口约定一致:果树 / 大田 / 蔬菜 */
@@ -121,6 +125,18 @@ const selectedItems = computed(() => {
     return list;
 });
 
+const nextBtnText = computed(() => {
+    if (!props.multiple) return "下一步 (2/4)";
+    const hasField = selectedItems.value.some(
+        (item) => item.firstCrop === "大田" || item.majorKey === "field"
+    );
+    const hasFruit = selectedItems.value.some(
+        (item) => item.firstCrop === "果树" || item.majorKey === "fruit"
+    );
+    const total = hasField && hasFruit ? 6 : 4;
+    return `下一步 (2/${total})`;
+});
+
 const mapHierarchyToGroups = (list) => {
     if (!Array.isArray(list)) return [];
     return list
@@ -135,18 +151,29 @@ const mapHierarchyToGroups = (list) => {
         .filter((group) => group.name && group.items.length);
 };
 
-const getSelectedId = () => {
+const cropTypeRank = (item) => {
+    if (item?.firstCrop === "大田" || item?.majorKey === "field") return 0;
+    if (item?.firstCrop === "果树" || item?.majorKey === "fruit") return 1;
+    return 2;
+};
+
+const sortFieldFirst = (list) =>
+    [...(list || [])].sort((a, b) => cropTypeRank(a) - cropTypeRank(b));
+
+const getCachedSelectedItems = () => {
     try {
         const raw = sessionStorage.getItem(SESSION_KEY);
-        if (!raw) return null;
+        if (!raw) return [];
         const list = JSON.parse(raw);
-        if (!Array.isArray(list) || !list.length) return null;
-        return String(list[0].id);
+        if (!Array.isArray(list)) return [];
+        return props.multiple ? list : list.slice(0, 1);
     } catch {
-        return null;
+        return [];
     }
 };
 
+const getSelectedIds = () => getCachedSelectedItems().map((item) => String(item.id));
+
 const clearAllSelection = () => {
     majorTabs.value.forEach((tab) => {
         tab.groups.forEach((group) => {
@@ -158,15 +185,32 @@ const clearAllSelection = () => {
 };
 
 const applySelectionToTab = (tab) => {
-    const selectedId = getSelectedId();
-    if (!selectedId) return;
+    const selectedIds = new Set(getSelectedIds());
+    if (!selectedIds.size) return;
     tab.groups.forEach((group) => {
         group.items.forEach((item) => {
-            item.selected = String(item.id) === selectedId;
+            item.selected = selectedIds.has(String(item.id));
         });
     });
 };
 
+const persistSelection = () => {
+    const loadedKeys = new Set(
+        majorTabs.value.filter((tab) => tab.loaded).map((tab) => tab.key)
+    );
+    const cachedFromUnloaded = getCachedSelectedItems().filter(
+        (item) => !loadedKeys.has(item.majorKey)
+    );
+    const list = props.multiple
+        ? sortFieldFirst([...cachedFromUnloaded, ...selectedItems.value])
+        : selectedItems.value.slice(0, 1);
+    if (list.length) {
+        sessionStorage.setItem(SESSION_KEY, JSON.stringify(list));
+    } else {
+        sessionStorage.removeItem(SESSION_KEY);
+    }
+};
+
 const fetchTabGroups = async (tabKey, force = false) => {
     const tab = majorTabs.value.find((item) => item.key === tabKey);
     if (!tab) return;
@@ -198,46 +242,45 @@ const handleSearch = () => {
 };
 
 const handleSelect = (item) => {
+    if (props.multiple) {
+        item.selected = !item.selected;
+        persistSelection();
+        return;
+    }
     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 = () => {
-    try {
-        const raw = sessionStorage.getItem(SESSION_KEY);
-        if (!raw) return [];
-        const list = JSON.parse(raw);
-        return Array.isArray(list) ? list.slice(0, 1) : [];
-    } catch {
-        return [];
-    }
+    persistSelection();
 };
 
 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 的缓存(仅一项)
-    return cached.filter((item) => !loadedKeys.has(item.majorKey)).slice(0, 1);
+    const cachedFromUnloaded = cached.filter((item) => !loadedKeys.has(item.majorKey));
+    if (props.multiple) {
+        const map = new Map();
+        [...cachedFromUnloaded, ...current].forEach((row) => {
+            map.set(String(row.id), row);
+        });
+        return sortFieldFirst(Array.from(map.values()));
+    }
+    if (current.length) return current.slice(0, 1);
+    return cachedFromUnloaded.slice(0, 1);
 };
 
 const handleNext = () => {
     const list = getSubmitSelectedItems();
     if (!list.length) {
-        ElMessage.warning("请选择一个种植品类");
+        ElMessage.warning(props.multiple ? "请选择种植品类" : "请选择一个种植品类");
         return;
     }
     sessionStorage.setItem(SESSION_KEY, JSON.stringify(list));
+    if (props.multiple) {
+        sessionStorage.removeItem("ENTRY_SELECTED_VARIETY_LIST");
+    }
     emit("next", list);
 };
 
@@ -365,33 +408,10 @@ watch(
             color: #1a1a1a;
 
             .title-icon {
-                position: relative;
-                width: 14px;
-                height: 14px;
+                width: 15px;
+                height: 15px;
+                object-fit: contain;
                 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;
-                }
             }
         }
 

+ 263 - 49
src/views/old_mini/entry_information/components/selectEquipment.vue

@@ -1,9 +1,9 @@
 <template>
-    <div class="select-equipment">
+    <div class="select-equipment" :class="{ 'is-service-equipment': isService }">
         <div class="select-equipment__content">
             <div class="page-header">
-                <div class="page-title">请填写您的农场设备</div>
-                <div class="page-subtitle">完善设备信息,让农机调度更精准</div>
+                <div class="page-title">{{ pageTitle }}</div>
+                <div class="page-subtitle">{{ pageSubtitle }}</div>
             </div>
 
             <div class="toolbar">
@@ -24,9 +24,10 @@
             <div class="equipment-list" v-loading="loading">
                 <div v-for="group in displayGroups" :key="group.name" class="equipment-card">
                     <div class="section-title">
-                        <span class="title-icon"></span>
+                        <img class="title-icon" src="@/assets/img/home/label-icon.png" alt="" />
                         <span>{{ group.name }}</span>
                     </div>
+                    <div v-if="group.desc" class="section-desc">{{ group.desc }}</div>
                     <div class="tag-group">
                         <div
                             v-for="item in group.items"
@@ -46,12 +47,17 @@
         <div class="custom-bottom-fixed-btns">
             <div class="btns-l">
                 <div class="bottom-btn secondary-btn" @click="emit('prev')">上一步</div>
-                <div class="bottom-btn secondary-btn" :class="{ disabled: submitting }" @click="handleSkip">
+                <div
+                    v-if="isLastStep && !isService"
+                    class="bottom-btn secondary-btn"
+                    :class="{ disabled: submitting }"
+                    @click="handleSkip"
+                >
                     跳过
                 </div>
             </div>
             <div class="bottom-btn primary-btn" :class="{ disabled: submitting }" @click="handleSubmit">
-                {{ submitting ? "提交中..." : "提交信息" }}
+                {{ primaryBtnText }}
             </div>
         </div>
 
@@ -64,8 +70,8 @@
         <tip-popup
             v-model:show="showSuccessPopup"
             type="executeSuccess"
-            text="您的信息已上传"
-            text2="请等待诊断报告生成"
+            :text="isService ? '您的信息已提交成功' : '您的信息已上传'"
+            :text2="isService ? '' : '请等待诊断报告生成'"
             buttonText="完成"
             @confirm="handleComplete"
         />
@@ -80,9 +86,16 @@ import { Search } from "@element-plus/icons-vue";
 import addMachinePopup from "./addMachinePopup.vue";
 import tipPopup from "@/components/popup/tipPopup.vue";
 
-const emit = defineEmits(["prev", "confirm"]);
+const emit = defineEmits(["prev", "next", "confirm"]);
+const props = defineProps({
+    isService: { type: Boolean, default: false },
+    isLastStep: { type: Boolean, default: true },
+    equipmentScope: { type: String, default: "" },
+    totalSteps: { type: Number, default: 4 },
+});
 
 const EQUIPMENT_KEY = "ENTRY_SELECTED_EQUIPMENT";
+const TASK_KEY = "ENTRY_SELECTED_FARM_TASKS";
 const SELECTED_LIST_KEY = "ENTRY_SELECTED_VARIETY_LIST";
 const CATEGORY_SESSION_KEY = "ENTRY_SELECTED_CATEGORY";
 const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
@@ -134,6 +147,99 @@ const DEFAULT_GROUPS = [
     },
 ];
 
+const FIELD_GROUPS = [
+    {
+        name: "耕地(整地、施肥、撒肥、中耕等)",
+        items: [
+            { id: 1001, name: "大中型拖拉机" },
+            { id: 1002, name: "手扶拖拉机" },
+            { id: 1003, name: "旋耕机" },
+            { id: 1004, name: "微耕机" },
+            { id: 1005, name: "挖掘机" },
+            { id: 1006, name: "培土机" },
+            { id: 1007, name: "撒肥机" },
+            { id: 1008, name: "秸秆还田机" },
+            { id: 1009, name: "开沟机" },
+            { id: 1010, name: "水田打浆机" },
+            { id: 1011, name: "深松机" },
+            { id: 1012, name: "起垄机" },
+            { id: 1013, name: "甘蔗开行机" },
+            { id: 1014, name: "棉花拔杆机" },
+        ],
+    },
+    {
+        name: "种(播种、育秧、移栽、补苗等)",
+        items: [
+            { id: 2001, name: "插秧机" },
+            { id: 2002, name: "一体育秧机" },
+            { id: 2003, name: "播种机" },
+            { id: 2004, name: "移栽机" },
+            { id: 2005, name: "无人机播种" },
+            { id: 2006, name: "小麦/玉米精量播种机" },
+            { id: 2007, name: "免耕播种机" },
+            { id: 2008, name: "花生覆膜播种机" },
+            { id: 2009, name: "马铃薯播种机" },
+        ],
+    },
+    {
+        name: "收(收割、烘干、运输等)",
+        items: [
+            { id: 3001, name: "联合收割机" },
+            { id: 3002, name: "半喂入收割机" },
+            { id: 3003, name: "玉米联合收割机" },
+            { id: 3004, name: "水稻收割机" },
+            { id: 3005, name: "烘干机" },
+            { id: 3006, name: "运粮车" },
+            { id: 3007, name: "挂车" },
+        ],
+    },
+];
+
+const FRUIT_GROUPS = [
+    {
+        name: "土壤与园地管理",
+        desc: "(整地、除草、清园、开沟修渠等)",
+        items: [
+            { id: 4001, name: "旋耕机" },
+            { id: 4002, name: "微耕机" },
+            { id: 4003, name: "挖掘机" },
+            { id: 4004, name: "旋耕开沟机" },
+            { id: 4005, name: "割草机" },
+            { id: 4006, name: "清沟机" },
+            { id: 4007, name: "运输车" },
+            { id: 4008, name: "开沟施肥机" },
+            { id: 4009, name: "树盘管理机" },
+            { id: 4010, name: "行间除草机" },
+            { id: 4011, name: "生草播种机" },
+            { id: 4012, name: "压草机" },
+        ],
+    },
+    {
+        name: "肥水管理",
+        desc: "(施肥、灌水、灌药等)",
+        items: [
+            { id: 5001, name: "水肥一体化" },
+            { id: 5002, name: "滴灌系统" },
+            { id: 5003, name: "注肥/注药泵" },
+            { id: 5004, name: "施肥枪" },
+            { id: 5005, name: "移动水车" },
+            { id: 5006, name: "水泵" },
+        ],
+    },
+    {
+        name: "植保打药",
+        desc: "(冠层打药、内膛打药等)",
+        items: [
+            { id: 6001, name: "植保无人机" },
+            { id: 6002, name: "风送式喷雾机" },
+            { id: 6003, name: "背负式喷雾器" },
+            { id: 6004, name: "柴油喷药机" },
+            { id: 6005, name: "烟雾机" },
+            { id: 6006, name: "高架喷雾机" },
+        ],
+    },
+];
+
 const displayGroups = computed(() => {
     const keyword = (appliedKeyword.value || "").trim();
     if (!keyword) return equipmentGroups.value;
@@ -153,6 +259,17 @@ const allEquipmentItems = computed(() =>
     equipmentGroups.value.flatMap((group) => group.items)
 );
 
+const pageTitle = computed(() =>
+    props.isService ? "完善设备信息" : "请填写您的农场设备"
+);
+const pageSubtitle = computed(() =>
+    props.isService ? "让农机调度更精准(可多选)" : "完善设备信息,让农机调度更精准"
+);
+const primaryBtnText = computed(() => {
+    if (submitting.value) return "提交中...";
+    return props.isLastStep ? "提交信息" : `下一步 (3/${props.isService ? props.totalSteps : 4})`;
+});
+
 function readJson(key) {
     try {
         const raw = sessionStorage.getItem(key);
@@ -162,25 +279,84 @@ function readJson(key) {
     }
 }
 
+function cloneGroups(groups) {
+    return groups.map((group) => ({
+        name: group.name,
+        desc: group.desc || "",
+        items: group.items.map((item) => ({ ...item })),
+    }));
+}
+
+function flattenEquipment(cached) {
+    if (!cached) return [];
+    if (Array.isArray(cached)) return cached;
+    return [...(cached.field || []), ...(cached.fruit || [])];
+}
+
+function getScopeList(cached, scope) {
+    if (!cached) return [];
+    if (Array.isArray(cached)) return scope === "fruit" ? [] : cached;
+    return cached[scope] || [];
+}
+
+function getCurrentScope() {
+    if (props.equipmentScope === "fruit") return "fruit";
+    if (props.equipmentScope === "field") return "field";
+    return "";
+}
+
 function restoreSelection() {
     const cached = readJson(EQUIPMENT_KEY);
-    if (!Array.isArray(cached)) return;
-    cached.forEach((item) => {
-        if (item?.id != null) selectedIds.add(String(item.id));
+    const scope = getCurrentScope();
+    const list = scope ? getScopeList(cached, scope) : flattenEquipment(cached);
+    list.forEach((item) => {
+        if (item?.id == null) return;
+        selectedIds.add(String(item.id));
+        if (!item.custom) return;
+        const exists = allEquipmentItems.value.some((row) => String(row.id) === String(item.id));
+        if (exists) return;
+        let group = equipmentGroups.value.find((row) => row.name === item.groupName);
+        if (!group) group = equipmentGroups.value[equipmentGroups.value.length - 1];
+        if (group) {
+            group.items.push({ id: item.id, name: item.name, custom: true });
+        }
+        if (Number(item.id) >= customMachineId) customMachineId = Number(item.id);
     });
 }
 
 function saveSelectionDraft() {
-    const selected = allEquipmentItems.value.filter((item) =>
-        selectedIds.has(String(item.id))
-    );
-    sessionStorage.setItem(EQUIPMENT_KEY, JSON.stringify(selected));
+    const scope = getCurrentScope();
+    const selected = allEquipmentItems.value
+        .filter((item) => selectedIds.has(String(item.id)))
+        .map((item) => {
+            const group = equipmentGroups.value.find((row) =>
+                row.items.some((rowItem) => String(rowItem.id) === String(item.id))
+            );
+            return { ...item, scope: scope || "farmer", groupName: group?.name };
+        });
+    if (!props.isService) {
+        sessionStorage.setItem(EQUIPMENT_KEY, JSON.stringify(selected));
+        return;
+    }
+    const cached = readJson(EQUIPMENT_KEY);
+    const next = {
+        field: Array.isArray(cached) ? cached : cached?.field || [],
+        fruit: Array.isArray(cached) ? [] : cached?.fruit || [],
+    };
+    if (scope === "fruit") next.fruit = selected;
+    else next.field = selected;
+    sessionStorage.setItem(EQUIPMENT_KEY, JSON.stringify(next));
 }
 
 function fetchEquipmentList() {
     loading.value = true;
-    // 暂无农机列表接口,使用本地默认数据
-    equipmentGroups.value = DEFAULT_GROUPS;
+    if (!props.isService) {
+        equipmentGroups.value = cloneGroups(DEFAULT_GROUPS);
+    } else if (props.equipmentScope === "fruit") {
+        equipmentGroups.value = cloneGroups(FRUIT_GROUPS);
+    } else {
+        equipmentGroups.value = cloneGroups(FIELD_GROUPS);
+    }
     loading.value = false;
 }
 
@@ -240,13 +416,19 @@ function getPhenophaseLabel(item) {
 function buildPlotPayload(includeEquipment = true) {
     const baForm = readJson("ENTRY_BA_FORM") || {};
     const varietyList = getVarietyDraftList();
-    const equipmentList = includeEquipment
-        ? allEquipmentItems.value.filter((item) => selectedIds.has(String(item.id)))
-        : [];
+    const equipmentCache = readJson(EQUIPMENT_KEY);
+    const equipmentList = includeEquipment ? flattenEquipment(equipmentCache) : [];
+    const taskCache = readJson(TASK_KEY);
+    const taskList = Array.isArray(taskCache)
+        ? taskCache
+        : [...(taskCache?.field || []), ...(taskCache?.fruit || [])];
 
     return {
         user_name: baForm.name,
         tel: baForm.phone,
+        team_name: baForm.teamName || "",
+        team_type: baForm.teamType || "",
+        invite_type: baForm.inviteType || (props.isService ? "service" : "farmer"),
         crops: varietyList.map((item) => ({
             crop_type: item.categoryName,
             crop_id: Number(item.categoryId),
@@ -260,6 +442,12 @@ function buildPlotPayload(includeEquipment = true) {
             id: item.id,
             name: item.name,
         })),
+        farm_tasks: taskList.map((item) => ({
+            id: item.id,
+            name: item.name,
+        })),
+        field_tasks: (taskCache?.field || []).map((item) => ({ id: item.id, name: item.name })),
+        fruit_tasks: (taskCache?.fruit || []).map((item) => ({ id: item.id, name: item.name })),
     };
 }
 
@@ -270,7 +458,10 @@ function clearEntrySession() {
     sessionStorage.removeItem(LOCATION_KEY);
     sessionStorage.removeItem(EDIT_UID_KEY);
     sessionStorage.removeItem(EQUIPMENT_KEY);
+    sessionStorage.removeItem(TASK_KEY);
+    sessionStorage.removeItem("ENTRY_MANUAL_VIEW");
     sessionStorage.removeItem(STEP_KEY);
+    sessionStorage.removeItem("ENTRY_INVITE_TYPE");
 }
 
 const showSuccessPopup = ref(false);
@@ -279,7 +470,7 @@ async function submitEntry(includeEquipment = true) {
     if (submitting.value) return;
     const varietyList = getVarietyDraftList();
 
-    if (!varietyList.length) {
+    if (!props.isService && !varietyList.length) {
         ElMessage.warning("请先完善种植品种信息");
         return;
     }
@@ -318,16 +509,25 @@ const handleComplete = () => {
 };
 
 const handleSkip = () => {
+    if (!props.isLastStep) {
+        emit("next");
+        return;
+    }
     submitEntry(false);
 };
 
 const handleSubmit = () => {
+    saveSelectionDraft();
+    if (!props.isLastStep) {
+        emit("next");
+        return;
+    }
     submitEntry(true);
 };
 
 onMounted(() => {
-    restoreSelection();
     fetchEquipmentList();
+    restoreSelection();
 });
 </script>
 
@@ -444,36 +644,20 @@ onMounted(() => {
         color: #1a1a1a;
 
         .title-icon {
-            position: relative;
-            width: 14px;
-            height: 14px;
+            width: 15px;
+            height: 15px;
+            object-fit: contain;
             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;
-            }
         }
     }
 
+    .section-desc {
+        margin: -2px 0 2px 22px;
+        font-size: 12px;
+        color: rgba(0, 0, 0, 0.4);
+        line-height: 18px;
+    }
+
     .tag-group {
         display: grid;
         grid-template-columns: repeat(3, 1fr);
@@ -573,5 +757,35 @@ onMounted(() => {
             color: #fff;
         }
     }
+
+    &.is-service-equipment {
+        .page-subtitle {
+            font-size: 16px;
+            font-weight: 600;
+            color: #005599;
+            line-height: 22px;
+        }
+
+        .tag-item {
+            height: auto;
+            min-height: 36px;
+            padding: 6px 4px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            line-height: 16px;
+
+            .text {
+                display: block;
+                white-space: normal;
+                overflow: visible;
+                text-overflow: unset;
+                font-size: 13px;
+                line-height: 16px;
+                padding: 0 4px;
+                word-break: break-all;
+            }
+        }
+    }
 }
 </style>

+ 76 - 24
src/views/old_mini/entry_information/components/selectVariety.vue

@@ -130,7 +130,7 @@
 
         <div class="custom-bottom-fixed-btns">
             <div class="bottom-btn secondary-btn" @click="handlePrev">上一步</div>
-            <div class="bottom-btn primary-btn" @click="handleConfirm">下一步 (3/4)</div>
+            <div class="bottom-btn primary-btn" @click="handleConfirm">{{ varietyNextText }}</div>
         </div>
     </div>
 </template>
@@ -144,9 +144,15 @@ import { useStore } from "vuex";
 import SelectLocationMap, { POINT_ICON } from "../map/selectLocationMap.js";
 
 const emit = defineEmits(["prev", "next"]);
+const props = defineProps({
+    multiple: { type: Boolean, default: false },
+    isLastStep: { type: Boolean, default: false },
+});
 const router = useRouter();
 const store = useStore();
 
+const varietyNextText = computed(() => (props.isLastStep ? "提交信息" : "下一步 (3/4)"));
+
 const DEFAULT_VISIBLE_COUNT = 12;
 const DEFAULT_POINT = "POINT(113.6142086995688 23.585836479509055)";
 const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
@@ -266,8 +272,7 @@ function restoreSelectedListDraft() {
             selectedUid = draft.selectedUid;
         }
         if (Array.isArray(draft?.list) && draft.list.length) {
-            // 单选:只保留一项
-            selectedList.value = draft.list.slice(0, 1);
+            selectedList.value = props.multiple ? draft.list : draft.list.slice(0, 1);
         }
     } catch {
         // ignore
@@ -324,7 +329,16 @@ const getSelectedCategoriesFromSession = () => {
         const raw = sessionStorage.getItem(CATEGORY_SESSION_KEY);
         if (!raw) return [];
         const list = JSON.parse(raw);
-        return Array.isArray(list) ? list : [];
+        if (!Array.isArray(list)) return [];
+        if (!props.multiple) return list;
+        return [...list].sort((a, b) => {
+            const rank = (item) => {
+                if (item?.firstCrop === "大田" || item?.majorKey === "field") return 0;
+                if (item?.firstCrop === "果树" || item?.majorKey === "fruit") return 1;
+                return 2;
+            };
+            return rank(a) - rank(b);
+        });
     } catch {
         return [];
     }
@@ -484,11 +498,24 @@ const createEmptyForm = (category) => ({
     location: "",
 });
 
-/** 始终保留一张可编辑的默认表单卡 */
+/** 始终保留可编辑表单卡:农户一张;农服按品类各一张 */
 const ensureDefaultForm = () => {
     const category = currentCategory.value;
     if (!category) {
-        selectedList.value = [];
+        if (!props.multiple) selectedList.value = [];
+        return;
+    }
+    if (props.multiple) {
+        const categoryIds = new Set(categoryTabs.value.map((tab) => String(tab.id)));
+        selectedList.value = selectedList.value.filter((item) =>
+            categoryIds.has(String(item.categoryId))
+        );
+        const exists = selectedList.value.some(
+            (item) => String(item.categoryId) === String(category.id)
+        );
+        if (!exists) {
+            selectedList.value.push(createEmptyForm(category));
+        }
         return;
     }
     if (!selectedList.value.length) {
@@ -500,8 +527,18 @@ const ensureDefaultForm = () => {
     item.categoryName = category.name;
 };
 
-/** 单选当前表单 */
-const currentSelected = computed(() => selectedList.value[0] || null);
+const getCurrentForm = () => {
+    if (props.multiple) {
+        return (
+            selectedList.value.find(
+                (item) => String(item.categoryId) === String(activeCategoryId.value)
+            ) || null
+        );
+    }
+    return selectedList.value[0] || null;
+};
+
+const currentSelected = computed(() => getCurrentForm());
 const hasVariety = computed(() => !!currentSelected.value?.id);
 const currentPhenologyOptions = computed(() =>
     getPhenologyOptions(currentSelected.value?.categoryId || activeCategoryId.value)
@@ -521,7 +558,7 @@ watch(activeCategoryId, (id) => {
 });
 
 const isSelected = (id) =>
-    !!id && selectedList.value.some((item) => String(item.id) === String(id));
+    !!id && String(getCurrentForm()?.id) === String(id);
 
 const handleSearch = () => {
     appliedKeyword.value = searchKeyword.value;
@@ -530,7 +567,7 @@ const handleSearch = () => {
 
 const applyVarietyToForm = (variety) => {
     ensureDefaultForm();
-    const item = selectedList.value[0];
+    const item = getCurrentForm();
     if (!item || !variety || !currentCategory.value) return;
     item.id = variety.id;
     item.name = variety.name;
@@ -552,7 +589,7 @@ const handleFormVarietyChange = (varietyId) => {
 
 const clearVariety = () => {
     ensureDefaultForm();
-    const item = selectedList.value[0];
+    const item = getCurrentForm();
     if (!item) return;
     item.id = "";
     item.name = "";
@@ -570,9 +607,8 @@ const onFormBodyClick = () => {
     }
 };
 
-/** 单选:点新品种写入当前表单;再点已选则清空品种 */
 const toggleVariety = (variety) => {
-    const current = selectedList.value[0];
+    const current = getCurrentForm();
     if (current && String(current.id) === String(variety.id)) {
         clearVariety();
         return;
@@ -601,30 +637,44 @@ const getBaForm = () => {
     }
 };
 
-const validateSubmitData = () => {
-    if (!categoryTabs.value.length) {
-        ElMessage.warning("请先选择种植品类");
-        return false;
-    }
-    const item = selectedList.value[0];
+const validateItem = (item, categoryName = "") => {
+    const label = item?.name || categoryName;
     if (!item?.id) {
-        ElMessage.warning("请选择一个品种");
+        ElMessage.warning(categoryName ? `请选择「${categoryName}」的品种` : "请选择一个品种");
         return false;
     }
     if (!item.phenologyId) {
-        ElMessage.warning(`请选择「${item.name}」的当下物候期`);
+        ElMessage.warning(`请选择「${label}」的当下物候期`);
         return false;
     }
     if (!item.startTime) {
-        ElMessage.warning(`请填写「${item.name}」的起种点时间`);
+        ElMessage.warning(`请填写「${label}」的起种点时间`);
         return false;
     }
     if (item.area === "" || item.area == null || Number(item.area) <= 0) {
-        ElMessage.warning(`请填写「${item.name}」的种植亩数`);
+        ElMessage.warning(`请填写「${label}」的种植亩数`);
         return false;
     }
     if (!item.location) {
-        ElMessage.warning(`请选择「${item.name}」的种植点位`);
+        ElMessage.warning(`请选择「${label}」的种植点位`);
+        return false;
+    }
+    return true;
+};
+
+const validateSubmitData = () => {
+    if (!categoryTabs.value.length) {
+        ElMessage.warning("请先选择种植品类");
+        return false;
+    }
+    if (props.multiple) {
+        for (const category of categoryTabs.value) {
+            const item = selectedList.value.find(
+                (row) => String(row.categoryId) === String(category.id)
+            );
+            if (!validateItem(item, category.name)) return false;
+        }
+    } else if (!validateItem(selectedList.value[0])) {
         return false;
     }
     const baForm = getBaForm();
@@ -636,6 +686,8 @@ const validateSubmitData = () => {
 };
 
 const handleConfirm = () => {
+    const currentCategoryName = currentCategory.value?.name || "";
+    if (props.multiple && !validateItem(getCurrentForm(), currentCategoryName)) return;
     const idx = categoryTabs.value.findIndex(
         (tab) => String(tab.id) === String(activeCategoryId.value)
     );

+ 428 - 0
src/views/old_mini/entry_information/components/serviceInformation.vue

@@ -0,0 +1,428 @@
+<template>
+    <div class="service-information">
+        <div class="service-information__content">
+            <div class="page-header">
+                <div class="page-title">完善个人信息</div>
+            </div>
+
+            <div class="info-card">
+                <div class="section-title">
+                    <img class="title-icon" src="@/assets/img/home/label-icon.png" alt="" />
+                    <span>基本信息</span>
+                </div>
+
+                <el-form
+                    ref="formRef"
+                    :model="form"
+                    :rules="rules"
+                    label-position="left"
+                    label-width="130px"
+                    class="info-form"
+                    require-asterisk-position="right"
+                >
+                    <el-form-item label="团队/合作社名称" prop="teamName">
+                        <el-input
+                            v-model="form.teamName"
+                            placeholder="请输入名字"
+                            maxlength="50"
+                            clearable
+                        />
+                    </el-form-item>
+                    <el-form-item label="负责人姓名" prop="leaderName">
+                        <el-input
+                            v-model="form.leaderName"
+                            placeholder="请输入名字"
+                            maxlength="20"
+                            clearable
+                        />
+                    </el-form-item>
+                    <el-form-item label="负责人电话" prop="leaderPhone">
+                        <el-input
+                            v-model="form.leaderPhone"
+                            placeholder="请输入电话"
+                            maxlength="11"
+                            clearable
+                            type="tel"
+                        />
+                    </el-form-item>
+                    <el-form-item label="团队类型(单选)" prop="teamType" class="team-type-form-item">
+                        <div class="team-type-grid">
+                            <div
+                                v-for="item in TEAM_TYPES"
+                                :key="item.value"
+                                class="team-type-item"
+                                :class="{ selected: form.teamType === item.value }"
+                                @click="selectTeamType(item.value)"
+                            >
+                                <span class="team-type-item__text">
+                                    <template v-for="(line, index) in item.lines" :key="index">
+                                        {{ line }}<br v-if="index < item.lines.length - 1" />
+                                    </template>
+                                </span>
+                            </div>
+                        </div>
+                    </el-form-item>
+                    <el-form-item label="所在位置" prop="location">
+                        <div
+                            class="location-value"
+                            :class="{ placeholder: !locationAddress }"
+                            @click="goSelectLocation"
+                        >
+                            {{ locationAddress || "具体定位" }}
+                        </div>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+
+        <div class="custom-bottom-fixed-btns">
+            <div class="bottom-btn primary-btn" @click="handleNext">下一步 (1/4)</div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { nextTick, onActivated, onMounted, reactive, ref } from "vue";
+import { useRouter } from "vue-router";
+import { convertPointToArray } from "@/utils/index";
+
+const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
+const FORM_KEY = "ENTRY_BA_FORM";
+const MAP_KEY = "CZLBZ-LJICQ-R4A5J-BN62X-YXCRJ-GNBUT";
+
+const TEAM_TYPES = [
+    { value: "machinery_coop", label: "农机专业合作社", lines: ["农机专业", "合作社"] },
+    { value: "grower", label: "种植大户兼营", lines: ["种植大户", "兼营"] },
+    { value: "family_farm", label: "家庭农场", lines: ["家庭农场"] },
+    { value: "agri_company", label: "农业公司", lines: ["农业公司"] },
+    { value: "individual", label: "个体农机户", lines: ["个体农机户"] },
+    { value: "other", label: "其它", lines: ["其它"] },
+];
+
+const emit = defineEmits(["next"]);
+const router = useRouter();
+
+const formRef = ref(null);
+const locationAddress = ref("");
+const form = reactive({
+    teamName: "",
+    leaderName: "",
+    leaderPhone: "",
+    teamType: "",
+    location: "",
+});
+
+const rules = {
+    teamName: [
+        { required: true, message: "请输入团队/合作社名称", trigger: "blur" },
+        { min: 1, max: 50, message: "名称长度不超过50个字", trigger: "blur" },
+    ],
+    leaderName: [
+        { required: true, message: "请输入负责人姓名", trigger: "blur" },
+        { min: 1, max: 20, message: "姓名长度不超过20个字", trigger: "blur" },
+    ],
+    leaderPhone: [
+        { required: true, message: "请输入负责人电话", trigger: "blur" },
+        {
+            pattern: /^1[3-9]\d{9}$/,
+            message: "请输入正确的手机号码",
+            trigger: "blur",
+        },
+    ],
+    teamType: [{ required: true, message: "请选择团队类型", trigger: "change" }],
+    location: [{ required: true, message: "请选择所在位置", trigger: "change" }],
+};
+
+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.leaderName,
+            phone: form.leaderPhone,
+            teamName: form.teamName,
+            leaderName: form.leaderName,
+            teamType: form.teamType,
+            inviteType: "service",
+        })
+    );
+}
+
+function restoreFormDraft() {
+    const draft = readJson(FORM_KEY);
+    if (!draft) return;
+    if (draft.teamName != null) form.teamName = draft.teamName;
+    if (draft.leaderName != null) form.leaderName = draft.leaderName;
+    else if (draft.name != null) form.leaderName = draft.name;
+    if (draft.leaderPhone != null) form.leaderPhone = draft.leaderPhone;
+    else if (draft.phone != null) form.leaderPhone = draft.phone;
+    if (draft.teamType != null) form.teamType = draft.teamType;
+}
+
+function fetchLocationAddress(point) {
+    const coordinate = convertPointToArray(point);
+    if (!coordinate?.length) return;
+    VE_API.old_mini_map
+        .location({
+            key: MAP_KEY,
+            location: `${coordinate[1]},${coordinate[0]}`,
+        })
+        .then(({ result }) => {
+            locationAddress.value =
+                result?.formatted_addresses?.recommend ||
+                result?.address ||
+                (result?.address_component
+                    ? result.address_component.city + (result.address_component.district || "")
+                    : "");
+        });
+}
+
+function syncLocationFromSession() {
+    const saved = readJson(LOCATION_KEY);
+    if (saved?.point) {
+        form.location = saved.point;
+        formRef.value?.clearValidate("location");
+        fetchLocationAddress(saved.point);
+        return;
+    }
+    form.location = "";
+    locationAddress.value = "";
+}
+
+const selectTeamType = (value) => {
+    form.teamType = value;
+    formRef.value?.clearValidate("teamType");
+};
+
+const goSelectLocation = () => {
+    saveFormDraft();
+    const saved = readJson(LOCATION_KEY);
+    const mapCenter = saved?.point || form.location || "";
+    router.push({
+        path: "/entry_select_location",
+        query: {
+            ...(mapCenter ? { mapCenter } : {}),
+            from: "entry_service",
+        },
+    });
+};
+
+const handleNext = async () => {
+    if (!formRef.value) return;
+    try {
+        await formRef.value.validate();
+        saveFormDraft();
+        emit("next", { ...form });
+    } catch {
+        // 校验未通过
+    }
+};
+
+onMounted(() => {
+    restoreFormDraft();
+    nextTick(() => syncLocationFromSession());
+});
+
+onActivated(() => {
+    restoreFormDraft();
+    nextTick(() => syncLocationFromSession());
+});
+</script>
+
+<style lang="scss" scoped>
+.service-information {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+    padding-bottom: 80px;
+
+    &__content {
+        flex: 1;
+        overflow-y: auto;
+        padding: 8px 16px 20px;
+    }
+
+    .page-header {
+        padding: 8px 4px 16px;
+
+        .page-title {
+            font-size: 26px;
+            color: #005599;
+            font-family: "PangMenZhengDao";
+            line-height: 36px;
+        }
+    }
+
+    .info-card {
+        background: #fff;
+        border-radius: 12px;
+        padding: 16px 14px 8px;
+        box-shadow: 0 2px 12px rgba(33, 153, 248, 0.08);
+    }
+
+    .section-title {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        margin-bottom: 8px;
+        font-size: 16px;
+        font-weight: 600;
+        color: #1a1a1a;
+
+        .title-icon {
+            width: 15px;
+            height: 15px;
+            object-fit: contain;
+            flex-shrink: 0;
+        }
+    }
+
+    .info-form {
+        :deep(.el-form-item) {
+            margin-bottom: 0;
+            padding: 10px 0;
+        }
+
+        :deep(.el-form-item__label) {
+            color: #1D2129;
+        }
+
+        :deep(.el-form-item__content) {
+            justify-content: flex-end;
+            line-height: 32px;
+        }
+
+        :deep(.el-form-item__error) {
+            padding-top: 4px;
+            text-align: right;
+        }
+
+        :deep(.el-input__wrapper) {
+            box-shadow: none;
+            background: transparent;
+            padding: 0;
+
+            .el-input__inner {
+                text-align: right;
+                color: #1a1a1a;
+
+                &::placeholder {
+                    color: rgba(29, 33, 41, 0.2);
+                }
+            }
+        }
+
+        .team-type-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;
+            }
+
+            :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;
+            }
+        }
+    }
+
+    .team-type-grid {
+        width: 100%;
+        display: grid;
+        grid-template-columns: repeat(3, 1fr);
+        gap: 8px;
+    }
+
+    .team-type-item {
+        position: relative;
+        width: 100%;
+        height: 40px;
+        padding: 2px 4px;
+        border-radius: 6px;
+        box-sizing: border-box;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        text-align: center;
+        color: #1a1a1a;
+        background: #f5f6f8;
+        border: 1px solid transparent;
+        overflow: hidden;
+        cursor: pointer;
+
+        &__text {
+            font-size: 12px;
+            line-height: 14px;
+        }
+
+        &.selected {
+            border-color: #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;
+            }
+        }
+    }
+
+    .location-value {
+        width: 100%;
+        text-align: right;
+        color: #2199f8;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+
+        &.placeholder {
+            color: #2199f8;
+        }
+    }
+
+    .custom-bottom-fixed-btns {
+        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>

+ 117 - 8
src/views/old_mini/entry_information/index.vue

@@ -2,33 +2,138 @@
     <div class="entry-information-page">
         <custom-header name="录入信息" bgColor="#fff"></custom-header>
         <div class="entry-information-body">
-            <ba-information v-if="currentStep === 1" @next="handleNext" />
-            <select-category v-else-if="currentStep === 2" @prev="handlePrev" @next="handleNext" />
-            <select-variety v-else-if="currentStep === 3" @prev="handlePrev" @next="handleNext" />
-            <select-equipment v-else-if="currentStep === 4" @prev="handlePrev" @confirm="handleConfirm" />
+            <service-information v-if="currentStep === 1 && isServiceEntry" @next="handleNext" />
+            <ba-information v-else-if="currentStep === 1" @next="handleNext" />
+            <select-category v-else-if="currentStep === 2" :multiple="isServiceEntry" @prev="handlePrev" @next="handleNext" />
+            <select-equipment
+                v-else-if="showEquipmentStep"
+                :key="equipmentScope || 'farmer'"
+                :is-service="isServiceEntry"
+                :is-last-step="equipmentIsLastStep"
+                :equipment-scope="equipmentScope"
+                :total-steps="totalSteps"
+                @prev="handlePrev"
+                @next="handleNext"
+                @confirm="handleConfirm"
+            />
+            <select-variety
+                v-else-if="showVarietyStep"
+                :multiple="isServiceEntry"
+                @prev="handlePrev"
+                @next="handleNext"
+            />
+            <manual-farming-service
+                v-else-if="showManualStep"
+                :has-field="hasFieldSelected"
+                :has-fruit="hasFruitSelected"
+                @prev="handlePrev"
+                @next="handleNext"
+                @confirm="handleConfirm"
+            />
         </div>
     </div>
 </template>
 
 <script setup>
-import { ref, watch } from "vue";
-import { useRouter } from "vue-router";
+import { computed, onActivated, onMounted, ref, watch } from "vue";
+import { useRoute, useRouter } from "vue-router";
 import customHeader from "@/components/customHeader.vue";
 import baInformation from "./components/baInformation.vue";
+import serviceInformation from "./components/serviceInformation.vue";
 import selectCategory from "./components/selectCategory.vue";
 import selectVariety from "./components/selectVariety.vue";
 import selectEquipment from "./components/selectEquipment.vue";
+import manualFarmingService from "./components/manualFarmingService.vue";
 
 const STEP_KEY = "ENTRY_INFORMATION_STEP";
+const INVITE_TYPE_KEY = "ENTRY_INVITE_TYPE";
+const CATEGORY_SESSION_KEY = "ENTRY_SELECTED_CATEGORY";
 
 const router = useRouter();
+const route = useRoute();
 
 function readStep() {
     const step = Number(sessionStorage.getItem(STEP_KEY));
-    return step >= 1 && step <= 4 ? step : 1;
+    return step >= 1 && step <= 5 ? step : 1;
+}
+
+function readInviteType() {
+    const fromQuery = route.query.inviteType;
+    if (fromQuery === "service" || fromQuery === "farmer") {
+        sessionStorage.setItem(INVITE_TYPE_KEY, fromQuery);
+        return fromQuery;
+    }
+    return sessionStorage.getItem(INVITE_TYPE_KEY) || "farmer";
 }
 
 const currentStep = ref(readStep());
+const inviteType = ref(readInviteType());
+const isServiceEntry = computed(() => inviteType.value === "service");
+const hasFieldSelected = ref(false);
+const hasFruitSelected = ref(false);
+const isBothCrops = computed(() => hasFieldSelected.value && hasFruitSelected.value);
+const totalSteps = computed(() => {
+    if (!isServiceEntry.value) return 4;
+    return isBothCrops.value ? 6 : 4;
+});
+const maxStep = computed(() => {
+    if (!isServiceEntry.value) return 4;
+    return isBothCrops.value ? 5 : 4;
+});
+const showEquipmentStep = computed(() => {
+    if (!isServiceEntry.value) return currentStep.value === 4;
+    if (hasFieldSelected.value && currentStep.value === 3) return true;
+    if (isBothCrops.value && currentStep.value === 5) return true;
+    if (hasFruitSelected.value && !hasFieldSelected.value && currentStep.value === 4) return true;
+    return false;
+});
+const equipmentScope = computed(() => {
+    if (!isServiceEntry.value) return "";
+    if (!hasFieldSelected.value) return "fruit";
+    return currentStep.value === 5 ? "fruit" : "field";
+});
+const equipmentIsLastStep = computed(() => {
+    if (!isServiceEntry.value) return true;
+    return equipmentScope.value === "fruit";
+});
+const showVarietyStep = computed(() => !isServiceEntry.value && currentStep.value === 3);
+const showManualStep = computed(() => {
+    if (!isServiceEntry.value) return false;
+    if (hasFieldSelected.value) return currentStep.value === 4;
+    return hasFruitSelected.value && currentStep.value === 3;
+});
+
+function refreshHasField() {
+    try {
+        const list = JSON.parse(sessionStorage.getItem(CATEGORY_SESSION_KEY) || "[]");
+        const items = Array.isArray(list) ? list : [];
+        hasFieldSelected.value = items.some(
+            (item) => item?.firstCrop === "大田" || item?.majorKey === "field"
+        );
+        hasFruitSelected.value = items.some(
+            (item) => item?.firstCrop === "果树" || item?.majorKey === "fruit"
+        );
+    } catch {
+        hasFieldSelected.value = false;
+        hasFruitSelected.value = false;
+    }
+}
+
+function clampStep() {
+    if (currentStep.value > maxStep.value) {
+        currentStep.value = maxStep.value;
+    }
+}
+
+function syncEntryState() {
+    inviteType.value = readInviteType();
+    currentStep.value = readStep();
+    refreshHasField();
+    clampStep();
+}
+
+onMounted(syncEntryState);
+onActivated(syncEntryState);
 
 watch(
     currentStep,
@@ -39,7 +144,11 @@ watch(
 );
 
 const handleNext = () => {
-    if (currentStep.value < 4) {
+    refreshHasField();
+    if (hasFieldSelected.value && currentStep.value === 3) {
+        sessionStorage.removeItem("ENTRY_MANUAL_VIEW");
+    }
+    if (currentStep.value < maxStep.value) {
         currentStep.value += 1;
     }
 };

+ 6 - 2
src/views/old_mini/entry_information/selectLocation.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="select-location">
-        <custom-header :name="fromGrowthReport ? '切换位置' : '选择种植点位'"></custom-header>
+        <custom-header :name="headerName"></custom-header>
         <div class="select-location-content">
             <div class="search-bar">
                 <location-search
@@ -40,6 +40,8 @@ const store = useStore();
 const mapContainer = ref(null);
 const isPlantPoint = route.query.pointType === "plant";
 const fromGrowthReport = route.query.from === "growth_report";
+const fromService = route.query.from === "entry_service";
+const headerName = fromGrowthReport ? "切换位置" : fromService ? "选择位置" : "选择种植点位";
 const sessionKey = fromGrowthReport ? SESSION_KEY_GROWTH : SESSION_KEY_RESIDENT;
 const iconSrc =
     route.query.iconType && POINT_ICON[route.query.iconType]
@@ -96,7 +98,9 @@ const handleLocate = () => {
 const handleSubmit = () => {
     const coordinate = mapLocation.data;
     if (!coordinate) {
-        ElMessage.warning(isPlantPoint ? "请选择种植点位" : "请选择常驻点位");
+        ElMessage.warning(
+            fromGrowthReport || fromService ? "请选择位置" : isPlantPoint ? "请选择种植点位" : "请选择常驻点位"
+        );
         return;
     }
     const point = toPointWkt(coordinate);

+ 18 - 5
src/views/old_mini/growth_report/index.vue

@@ -57,7 +57,7 @@
         </div>
 
         <div class="invite-group" :style="{ bottom: `${inviteBottom}px` }">
-            <div class="invite-btn">
+            <div class="invite-btn" @click="handleInvite('service')">
                 <img class="invite-icon" src="@/assets/img/home/user-icon.png" alt="">
                 邀请农服</div>
             <div class="invite-btn" @click="handleInvite('farmer')">
@@ -217,7 +217,8 @@ const handleSelectMenu = (key) => {
     activeMenuKey.value = key;
 };
 
-const handleInvite = (item) => {
+const handleInvite = (type) => {
+    const isService = type === "service";
     let inviteName = "好友";
     try {
         const userInfo = JSON.parse(localStorage.getItem("localUserInfo") || "{}");
@@ -225,11 +226,12 @@ const handleInvite = (item) => {
     } catch {
         // ignore
     }
+    const inviteType = isService ? "service" : "farmer";
     const query = {
         askInfo: { title: "邀请完善信息", content: "是否分享该邀请给好友" },
-        shareText: "邀请您完善地块种植信息",
+        shareText: isService ? "邀请您完善农服信息" : "邀请您完善地块种植信息",
         targetUrl: `growth_report`,
-        paramsPage: JSON.stringify({ inviteName, showInviteEntry: 1 }),
+        paramsPage: JSON.stringify({ inviteName, showInviteEntry: 1, inviteType }),
         imageUrl: 'https://birdseye-img.sysuimars.com/temp/field.png',
     };
     wx.miniProgram.navigateTo({
@@ -251,6 +253,16 @@ function parseMaybeJson(value) {
     }
 }
 
+function getInviteTypeFromRoute() {
+    const candidates = [
+        route.query.inviteType,
+        parseMaybeJson(route.query.paramsPage)?.inviteType,
+        parseMaybeJson(route.query.miniJson)?.inviteType,
+        parseMaybeJson(parseMaybeJson(route.query.miniJson)?.paramsPage)?.inviteType,
+    ];
+    return candidates.find((item) => item === "service" || item === "farmer") || "farmer";
+}
+
 function shouldShowInviteEntryPopup() {
     if (isTruthyFlag(route.query.showInviteEntry)) return true;
 
@@ -297,8 +309,9 @@ function clearInviteEntryQuery() {
 
 function tryOpenInviteEntryPopup() {
     if (!shouldShowInviteEntryPopup()) return;
+    const inviteType = getInviteTypeFromRoute();
     nextTick(() => {
-        invitePopupRef.value?.open();
+        invitePopupRef.value?.open(inviteType);
         clearInviteEntryQuery();
     });
 }