lxf 19 годин тому
батько
коміт
b12497b5d9

+ 2 - 2
src/App.vue

@@ -25,7 +25,7 @@
     <Tabbar class="tabbar" route fixed v-show="showTab" active-color="#2199F8" inactive-color="#898989">
         <!-- 托管农户:首页、农情档案、农事记录 -->
         <!-- <template v-if="userType == 2"> -->
-            <!-- <tabbar-item replace to="/growth_report">
+            <tabbar-item replace to="/growth_report">
                 <span>{{ t("tabbar.growthReport") }}</span>
                 <template #icon="props">
                     <img
@@ -36,7 +36,7 @@
                         "
                     />
                 </template>
-            </tabbar-item> -->
+            </tabbar-item>
             <tabbar-item replace to="/agri_file">
                 <span>{{ t("tabbar.agriFile") }}</span>
                 <template #icon="props">

+ 9 - 0
src/api/modules/entry.js

@@ -0,0 +1,9 @@
+const config = require("../config")
+
+module.exports = {
+    // 获取种植品类列表
+    getCropHierarchy: {
+        url: config.base_new_url + "get_crop_hierarchy",
+        type: "get",
+    },
+}

BIN
src/assets/img/home/plant.png


+ 10 - 0
src/views/old_mini/entry_information/components/baInformation.vue

@@ -53,6 +53,8 @@
         <div class="custom-bottom-fixed-btns">
             <div class="bottom-btn primary-btn" @click="handleNext">下一步 (1/3)</div>
         </div>
+
+        <invite-entry-popup ref="invitePopupRef" />
     </div>
 </template>
 
@@ -61,9 +63,11 @@ import { nextTick, onActivated, onBeforeUnmount, onMounted, reactive, ref } from
 import { useRouter } from "vue-router";
 import { useStore } from "vuex";
 import SelectLocationMap from "../map/selectLocationMap.js";
+import inviteEntryPopup from "./inviteEntryPopup.vue";
 
 const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
 const FORM_KEY = "ENTRY_BA_FORM";
+const INVITE_POPUP_KEY = "ENTRY_INVITE_POPUP_SHOWN";
 const DEFAULT_POINT = "POINT(113.6142086995688 23.585836479509055)";
 
 const emit = defineEmits(["next"]);
@@ -72,6 +76,7 @@ const store = useStore();
 
 const formRef = ref(null);
 const previewMapRef = ref(null);
+const invitePopupRef = ref(null);
 let previewMap = null;
 const form = reactive({
     name: "",
@@ -197,6 +202,11 @@ const handleNext = async () => {
 onMounted(() => {
     restoreFormDraft();
     syncLocationFromSession();
+    // 首次进入本页弹出邀请录入弹窗
+    if (!sessionStorage.getItem(INVITE_POPUP_KEY)) {
+        sessionStorage.setItem(INVITE_POPUP_KEY, "1");
+        nextTick(() => invitePopupRef.value?.open());
+    }
 });
 
 onActivated(() => {

+ 92 - 0
src/views/old_mini/entry_information/components/inviteEntryPopup.vue

@@ -0,0 +1,92 @@
+<template>
+    <Popup
+        v-model:show="show"
+        round
+        class="invite-entry-popup"
+        teleport="body"
+        :z-index="9999"
+        :close-on-click-overlay="false"
+    >
+        <div class="invite-entry-popup__inner">
+            <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">
+                    录入 <span class="highlight">地块种植信息</span>
+                </div>
+            </div>
+            <div class="invite-entry-popup__btn" @click="handleConfirm">去录入</div>
+        </div>
+    </Popup>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { useRoute } from "vue-router";
+import { Popup } from "vant";
+
+const show = ref(false);
+
+const open = () => {
+    show.value = true;
+};
+
+const close = () => {
+    show.value = false;
+};
+
+const handleConfirm = () => {
+    close();
+};
+
+defineExpose({ open, close });
+</script>
+
+<style lang="scss">
+.invite-entry-popup {
+    width: 78%;
+    max-width: 320px;
+    overflow: hidden;
+    background: #fff;
+
+    .invite-entry-popup__inner {
+        padding: 10px;
+        text-align: center;
+    }
+
+    .invite-entry-popup__cover {
+        width: 100%;
+        height: 158px;
+        object-fit: cover;
+        border-radius: 12px;
+        display: block;
+    }
+
+    .invite-entry-popup__text {
+        margin-top: 12px;
+        font-size: 24px;
+        color: #111;
+        line-height: 28px;
+    }
+
+    .invite-entry-popup__line {
+        font-family: "PangMenZhengDao";
+        .highlight {
+            color: #2199f8;
+        }
+    }
+
+    .invite-entry-popup__btn {
+        margin-top: 12px;
+        height: 40px;
+        line-height: 40px;
+        width: 160px;
+        margin: 12px auto 0;
+        text-align: center;
+        border-radius: 22px;
+        background: #2199f8;
+        color: #fff;
+        font-size: 16px;
+    }
+}
+</style>

+ 98 - 117
src/views/old_mini/entry_information/components/selectCategory.vue

@@ -30,7 +30,7 @@
                 <div class="search-btn" @click="handleSearch">搜索</div>
             </div>
 
-            <div class="category-list">
+            <div class="category-list" v-loading="loading">
                 <div
                     v-for="group in displayGroups"
                     :key="group.name"
@@ -52,7 +52,7 @@
                         </div>
                     </div>
                 </div>
-                <div v-if="!displayGroups.length" class="empty-tip">暂无匹配品类</div>
+                <div v-if="!loading && !displayGroups.length" class="empty-tip">暂无匹配品类</div>
             </div>
         </div>
 
@@ -64,7 +64,7 @@
 </template>
 
 <script setup>
-import { computed, onMounted, ref } from "vue";
+import { computed, ref, watch } from "vue";
 import { ElMessage } from "element-plus";
 import { Search } from "@element-plus/icons-vue";
 
@@ -72,109 +72,17 @@ const SESSION_KEY = "ENTRY_SELECTED_CATEGORY";
 
 const emit = defineEmits(["prev", "next"]);
 
+/** first_crop 与接口约定一致:果树 / 大田 / 蔬菜 */
 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 },
-                ],
-            },
-        ],
-    },
+    { key: "fruit", label: "果树类", firstCrop: "果树", groups: [], loaded: false },
+    { key: "field", label: "大田类", firstCrop: "大田", groups: [], loaded: false },
+    { key: "vegetable", label: "蔬菜类", firstCrop: "蔬菜", groups: [], loaded: false },
 ]);
 
 const activeTabKey = ref("fruit");
 const searchKeyword = ref("");
 const appliedKeyword = ref("");
+const loading = ref(false);
 
 const activeTab = computed(() =>
     majorTabs.value.find((tab) => tab.key === activeTabKey.value) || majorTabs.value[0]
@@ -204,6 +112,7 @@ const selectedItems = computed(() => {
                         groupName: group.name,
                         majorKey: tab.key,
                         majorLabel: tab.label,
+                        firstCrop: tab.firstCrop,
                     });
                 }
             });
@@ -212,6 +121,66 @@ const selectedItems = computed(() => {
     return list;
 });
 
+const mapHierarchyToGroups = (list) => {
+    if (!Array.isArray(list)) return [];
+    return list
+        .map((group) => ({
+            name: group.second_type || group.name || "",
+            items: (group.third_crops || []).map((crop) => ({
+                id: crop.id,
+                name: crop.name,
+                selected: false,
+            })),
+        }))
+        .filter((group) => group.name && group.items.length);
+};
+
+const getSelectedIdSet = () => {
+    try {
+        const raw = sessionStorage.getItem(SESSION_KEY);
+        if (!raw) return new Set();
+        return new Set(JSON.parse(raw).map((item) => String(item.id)));
+    } catch {
+        return new Set();
+    }
+};
+
+const applySelectionToTab = (tab) => {
+    const selectedIds = getSelectedIdSet();
+    if (!selectedIds.size) return;
+    tab.groups.forEach((group) => {
+        group.items.forEach((item) => {
+            item.selected = selectedIds.has(String(item.id));
+        });
+    });
+};
+
+const fetchTabGroups = async (tabKey, force = false) => {
+    const tab = majorTabs.value.find((item) => item.key === tabKey);
+    if (!tab) return;
+    if (tab.loaded && !force) return;
+
+    loading.value = true;
+    try {
+        const res = await VE_API.entry.getCropHierarchy({
+            first_crop: tab.firstCrop,
+        });
+        if (res.code === 200) {
+            tab.groups = mapHierarchyToGroups(res.data);
+            tab.loaded = true;
+            applySelectionToTab(tab);
+        } else {
+            tab.groups = [];
+            ElMessage.error(res.msg || "获取种植品类失败");
+        }
+    } catch {
+        tab.groups = [];
+        ElMessage.error("获取种植品类失败,请稍后再试");
+    } finally {
+        loading.value = false;
+    }
+};
+
 const handleSearch = () => {
     appliedKeyword.value = searchKeyword.value.trim();
 };
@@ -220,35 +189,47 @@ const handleSelect = (item) => {
     item.selected = !item.selected;
 };
 
-const restoreSelection = () => {
+const getCachedSelectedItems = () => {
     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);
-                });
-            });
-        });
+        if (!raw) return [];
+        const list = JSON.parse(raw);
+        return Array.isArray(list) ? list : [];
     } catch {
-        // ignore
+        return [];
     }
 };
 
+const getSubmitSelectedItems = () => {
+    const current = selectedItems.value;
+    const cached = getCachedSelectedItems();
+    const loadedKeys = new Set(
+        majorTabs.value.filter((tab) => tab.loaded).map((tab) => tab.key)
+    );
+    // 已加载 Tab 以当前勾选为准;未加载 Tab 保留缓存里的选择
+    const fromCache = cached.filter((item) => !loadedKeys.has(item.majorKey));
+    return [...current, ...fromCache];
+};
+
 const handleNext = () => {
-    if (!selectedItems.value.length) {
+    const list = getSubmitSelectedItems();
+    if (!list.length) {
         ElMessage.warning("请至少选择一个种植品类");
         return;
     }
-    sessionStorage.setItem(SESSION_KEY, JSON.stringify(selectedItems.value));
-    emit("next", selectedItems.value);
+    sessionStorage.setItem(SESSION_KEY, JSON.stringify(list));
+    emit("next", list);
 };
 
-onMounted(() => {
-    restoreSelection();
-});
+watch(
+    activeTabKey,
+    (key) => {
+        appliedKeyword.value = "";
+        searchKeyword.value = "";
+        fetchTabGroups(key);
+    },
+    { immediate: true },
+);
 </script>
 
 <style lang="scss" scoped>

+ 59 - 0
src/views/old_mini/growth_report/index.vue

@@ -29,6 +29,15 @@
             </div>
             <div class="map-container" ref="mapContainer"></div>
         </div>
+        <div class="invite-group">
+            <div class="invite-btn" @click="handleInvite('farmer')">
+                <img class="invite-icon" src="@/assets/img/home/user-icon.png" alt="">
+                邀请农服</div>
+            <div class="invite-btn">
+                <img class="invite-icon" src="@/assets/img/home/user-icon.png" alt="">
+                邀请农户</div>
+        </div>
+
         <risk-report-panel
             v-show="activeGardenTab === 'current'"
             :view-type="panelViewType"
@@ -49,6 +58,7 @@ import gardenList from "@/components/gardenList.vue";
 import GrowthReportMap from "./growthReportMap.js";
 import RiskReportPanel from "./components/RiskReportPanel.vue";
 import * as util from "@/common/ol_common.js";
+import wx from "weixin-js-sdk";
 
 const DEFAULT_FARM_POINT = "POINT(113.6142086995688 23.585836479509055)";
 
@@ -240,6 +250,27 @@ const changeGarden = (data) => {
     syncMapByFarm(data);
 };
 
+
+const handleInvite = (item) => {
+    let inviteName = "好友";
+    try {
+        const userInfo = JSON.parse(localStorage.getItem("localUserInfo") || "{}");
+        inviteName = userInfo.nickName || userInfo.userName || userInfo.name || inviteName;
+    } catch {
+        // ignore
+    }
+    const query = {
+        askInfo: { title: "邀请完善信息", content: "是否分享该邀请给好友" },
+        shareText: "邀请您完善地块种植信息",
+        targetUrl: `entry_information`,
+        paramsPage: JSON.stringify({ inviteName }),
+        imageUrl: 'https://birdseye-img.sysuimars.com/temp/field.png',
+    };
+    wx.miniProgram.navigateTo({
+        url: `/pages/subPages/share_page/index?pageParams=${JSON.stringify(query)}&type=sharePage`,
+    });
+}
+
 onActivated(async () => {
     if (route.query?.farmId) {
         defaultGardenId.value = route.query.farmId;
@@ -291,6 +322,34 @@ onActivated(async () => {
             }
         }
     }
+    .invite-group {
+        position: fixed;
+        bottom: 300px;
+        right: 16px;
+        z-index: 10;
+        pointer-events: none;
+        .invite-btn {
+            pointer-events: auto;
+            height: 32px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            border-radius: 5px;
+            background: #fff;
+            box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.1);
+            gap: 4px;
+            padding: 0 8px;
+            color: #2199F8;
+            font-weight: 500;
+            font-size: 12px;
+            .invite-icon {
+                width: 16px;
+            }
+        }
+        .invite-btn + .invite-btn {
+            margin-top: 10px;
+        }
+    }
 
     .report-content {
         position: relative;