lxf 1 тиждень тому
батько
коміт
c9396cc95e

+ 15 - 0
src/api/modules/questionnaire.js

@@ -11,4 +11,19 @@ module.exports = {
         url: config.base_new_url + "pheno/interact_record",
         type: "post",
     },
+    // 查看报告是否生成
+    checkReportGenerated: {
+        url: config.base_new_url + "report/check",
+        type: "get",
+    },
+    // 获取外部感知任务接口
+    getExternalInteractTasks: {
+        url: config.base_new_url + "pheno/interact_tasks",
+        type: "get",
+    },
+    // 获取内部感知任务接口
+    getInternalInteractTasks: {
+        url: config.base_new_url + "pheno/inner_interact_tasks",
+        type: "get",
+    },
 }

BIN
src/assets/img/common/logo.png


+ 1 - 1
src/components/farmHeader.vue

@@ -25,7 +25,7 @@ import { useI18n } from "@/i18n";
 const { t } = useI18n();
 const router = useRouter();
 
-const defaultAvatar = require("@/assets/img/home/banner.png");
+const defaultAvatar = require("@/assets/img/common/logo.png");
 const farmName = ref("");
 const farmLocation = ref("");
 const circleUrl = ref(defaultAvatar);

+ 1 - 1
src/components/pageComponents/FarmWorkPlanTimeline.vue

@@ -486,7 +486,7 @@ const getFarmWorkPlan = () => {
                               progress2: Number(it.progress2) || 0, // 终点 %
                               startDate: it.startDate,
                               startTimeMs: safeParseDate(
-                                  it.startDate || it.beginDate || it.startTime || it.start || it.start_at
+                                  it.startDate || it.beginDate || it.start || it.start_at
                               ),
                               reproductiveList,
                           };

+ 75 - 15
src/components/popup/diagnosisReportPopup.vue

@@ -1,6 +1,6 @@
 <template>
     <popup
-        v-model:show="showValue"
+        v-model:show="visible"
         round
         teleport="body"
         class="diagnosis-report-popup"
@@ -18,30 +18,90 @@
 </template>
 
 <script setup>
-import { computed } from "vue";
+import { onActivated, onMounted, ref, watch } from "vue";
+import { useRouter } from "vue-router";
 import { Popup } from "vant";
 import { useI18n } from "@/i18n";
 
 const { t } = useI18n();
+const router = useRouter();
 
-const props = defineProps({
-    show: {
-        type: Boolean,
-        default: false,
-    },
-});
+const ENTRY_FARM_DATA_KEY = "ENTRY_FARM_DATA";
+const ENTRY_BA_FORM_KEY = "ENTRY_BA_FORM";
+const SHOWN_CACHE_PREFIX = "DIAGNOSIS_REPORT_POPUP_SHOWN_";
 
-const emit = defineEmits(["update:show", "confirm"]);
+/** 弹窗显隐由组件内接口控制,默认不展示 */
+const visible = ref(false);
 
-const showValue = computed({
-    get: () => props.show,
-    set: (val) => emit("update:show", val),
-});
+const readEntryFarmPhone = () => {
+    try {
+        const farm = JSON.parse(localStorage.getItem(ENTRY_FARM_DATA_KEY) || "null");
+        if (farm?.phone) return String(farm.phone).trim();
+    } catch {
+        /* ignore */
+    }
+    try {
+        const baForm = JSON.parse(sessionStorage.getItem(ENTRY_BA_FORM_KEY) || "null");
+        if (baForm?.phone) return String(baForm.phone).trim();
+    } catch {
+        /* ignore */
+    }
+    try {
+        const userInfo = JSON.parse(localStorage.getItem("localUserInfo") || "{}");
+        return String(userInfo.tel || userInfo.phone || userInfo.mobile || "").trim();
+    } catch {
+        return "";
+    }
+};
+
+const getShownCacheKey = (tel) => `${SHOWN_CACHE_PREFIX}${tel}`;
+const hasShownPopup = (tel) => !!localStorage.getItem(getShownCacheKey(tel));
+
+const markPopupShown = (tel) => {
+    if (!tel) return;
+    localStorage.setItem(getShownCacheKey(tel), "1");
+};
+
+/** exists=2 且本地未展示过时弹窗(不依赖物候确认) */
+const checkShouldShow = async () => {
+    const tel = readEntryFarmPhone();
+    if (!tel || hasShownPopup(tel)) {
+        visible.value = false;
+        return;
+    }
+    try {
+        const res = await VE_API.questionnaire.checkReportGenerated({ tel });
+        const shouldShow = res?.code === 200 && Number(res?.data?.exists) === 2;
+        visible.value = shouldShow;
+        if (shouldShow) markPopupShown(tel);
+    } catch {
+        visible.value = false;
+    }
+};
 
 const handleView = () => {
-    emit("confirm");
-    showValue.value = false;
+    const tel = readEntryFarmPhone();
+    markPopupShown(tel);
+    visible.value = false;
+    router.push("/diagnosis_report");
 };
+
+// 用户点遮罩关闭时也记为已展示,避免反复弹出
+watch(visible, (val, oldVal) => {
+    if (oldVal && !val) markPopupShown(readEntryFarmPhone());
+});
+
+onMounted(() => {
+    checkShouldShow();
+});
+
+onActivated(() => {
+    checkShouldShow();
+});
+
+defineExpose({
+    checkShouldShow,
+});
 </script>
 
 <style scoped lang="scss">

+ 1 - 1
src/i18n/messages.js

@@ -179,7 +179,7 @@ export default {
             reportGenerated: "已生成",
             clickToView: "点击查看",
             viewInDiagnosisHint: "还可在 农事档案-诊断报告 中查看",
-            initialReport: "初次报告",
+            initialReport: "种植诊断报告",
             forwardReport: "转发报告",
             confirmLeaveTitle: "确认要离开?",
             confirmLeaveDesc1: "长势巡园比例还没回答,确定要走吗?",

+ 64 - 20
src/views/old_mini/agri_file/index.vue

@@ -139,10 +139,8 @@
         <album-upload-popup v-model:show="showUploadPopup" />
         <complete-farm-info-popup v-model:show="showCompleteFarmPopup" @confirm="goCompleteFarmInfo" />
 
-        <!-- 诊断报告弹窗 -->
-        <diagnosis-report-popup 
-            v-model:show="showDiagnosisReportPopup"
-            @confirm="goDiagnosisReport" />
+        <!-- 诊断报告弹窗:组件内接口判断 exists=2 且本地未展示过 -->
+        <diagnosis-report-popup />
 
         <!-- 恢复种植弹窗 -->
         <restore-planting-popup />
@@ -247,7 +245,6 @@ const reportList = ref([
 
 const showUploadPopup = ref(false);
 const showCompleteFarmPopup = ref(false);
-const showDiagnosisReportPopup = ref(false);
 // 后续由接口控制是否展示代管授权弹窗
 const showProxyAuthPopup = ref(false);
 const proxyServiceName = ref("农服名称");
@@ -277,6 +274,7 @@ const handlePhenologyUpdateReject = () => {
 const handlePhenologyUpdateConfirm = () => {
     // TODO: 调用确认更新接口
 };
+const PATROL_ICON = require("@/assets/img/report/wh-icon.png");
 const patrolList = ref([
     {
         id: 1,
@@ -286,7 +284,8 @@ const patrolList = ref([
         title: "",
         subject: "",
         issue: "",
-        icon: require("@/assets/img/report/wh-icon.png"),
+        desc: "",
+        icon: PATROL_ICON,
     },
     {
         id: 2,
@@ -296,10 +295,49 @@ const patrolList = ref([
         title: "",
         subject: "",
         issue: "",
-        icon: require("@/assets/img/report/wh-icon.png"),
+        icon: PATROL_ICON,
     },
 ]);
 
+/** 外部感知任务 → 巡园卡片(仅取第一条) */
+const mapInteractTaskToPatrol = (task) => {
+    const content = task?.interact_content || {};
+    return {
+        id: task.zone_id || task.interact_code,
+        type: "growth",
+        theme: "blue",
+        level: content.interact_level || "",
+        title: t("agriFile.patrolGrowthTitle") || "",
+        desc: content.interact_reason || "",
+        issue: content.interact_reason || "",
+        subject: content.interact_title || "",
+        icon: PATROL_ICON,
+        zoneId: task.zone_id,
+        interactCode: task.interact_code,
+        raw: task,
+    };
+};
+
+const fetchPatrolInteractTask = async () => {
+    try {
+        const res = await VE_API.questionnaire.getExternalInteractTasks({ zone_id: 42 });
+        const tasks = res?.data?.interact_tasks;
+        const first = Array.isArray(tasks) ? tasks[0] : null;
+        if (!first) return;
+        const mapped = mapInteractTaskToPatrol(first);
+        if (patrolList.value.length) {
+            patrolList.value[0] = {
+                ...patrolList.value[0],
+                ...mapped,
+            };
+        } else {
+            patrolList.value = [mapped];
+        }
+    } catch (e) {
+        console.warn("[agri_file] getExternalInteractTasks failed", e);
+    }
+};
+
 const cropArchiveList = ref([
     { id: 1, date: "2025-04-18", zone_name: "", content: "" },
     { id: 2, date: "2025-04-18", zone_name: "", content: "" },
@@ -376,14 +414,18 @@ const fillMockLabels = () => {
         desc: t("agriFile.reportDesc"),
         date: t("agriFile.reportDate"),
     }));
-    patrolList.value = patrolList.value.map((item, index) => ({
-        ...item,
-        level: t("agriFile.riskLevel2"),
-        title: t(index === 0 ? "agriFile.patrolGrowthTitle" : "agriFile.patrolAbnormalTitle"),
-        zone: albumName,
-        subject: t("agriFile.patrolSubject"),
-        issue: t("agriFile.patrolIssue"),
-    }));
+    patrolList.value = patrolList.value.map((item, index) => {
+        // 第一条由外部感知任务接口填充,不覆盖
+        if (index === 0 && item.interactCode) return item;
+        return {
+            ...item,
+            level: t("agriFile.riskLevel2"),
+            title: t(index === 0 ? "agriFile.patrolGrowthTitle" : "agriFile.patrolAbnormalTitle"),
+            zone: albumName,
+            subject: t("agriFile.patrolSubject"),
+            issue: t("agriFile.patrolIssue"),
+        };
+    });
     cropArchiveList.value = cropArchiveList.value.map((item, index) => ({
         ...item,
         zone_name: albumName,
@@ -410,7 +452,11 @@ const goAlbumMap = () => {
 const goGrowthTrack = (item) => {
     router.push({
         path: "/growth_track",
-        query: { id: item?.id, type: item?.type || "growth" },
+        query: {
+            id: item?.id,
+            type: item?.type || "growth",
+            pageName: item.type === 'growth' ? t("agriFile.patrolGrowthTitle") : t("agriFile.patrolAbnormalTitle"),
+        },
     });
 };
 
@@ -497,10 +543,6 @@ const goCompleteFarmInfo = () => {
     router.push("/entry_information");
 };
 
-const goDiagnosisReport = () => {
-    router.push("/diagnosis_report");
-};
-
 const goAlbumDetail = (item) => {
     router.push({
         path: "/region_albums",
@@ -514,12 +556,14 @@ const goAlbumDetail = (item) => {
 
 onMounted(() => {
     fillMockLabels();
+    fetchPatrolInteractTask();
     initAlbumMap();
     tryShowGuide();
     document.addEventListener("click", handleOutsideClick, true);
 });
 onActivated(() => {
     fillMockLabels();
+    fetchPatrolInteractTask();
     initAlbumMap();
     tryShowGuide();
 });

+ 98 - 18
src/views/old_mini/agri_file/pages/growthTrack.vue

@@ -1,7 +1,7 @@
 <template>
     <div class="growth-track-page">
         <custom-header
-            :name="t('agriFile.workName')"
+            :name="pageTitle"
             :isGoBack="!isFromShare"
             :isClose="isFromShare"
             :showClose="false"
@@ -32,7 +32,7 @@
             <template v-if="!isAbnormal">
                 <div class="growth-content">
                     <div class="growth-content__title">{{ t("agriFile.agriAssessment") }}</div>
-                    <div class="growth-content__desc">{{ t("agriFile.agriAssessmentDesc") }}</div>
+                    <div class="growth-content__desc">{{ assessmentDesc }}</div>
                 </div>
                 <div class="interact-card">
                     <div class="interact-card__head">
@@ -42,7 +42,7 @@
                         </div>
                     </div>
                     <div class="interact-card__qa">
-                        <div class="interact-card__question">{{ t("agriFile.interactQuestionText") }}</div>
+                        <div class="interact-card__question">{{ interactProblem }}</div>
                         <div class="interact-card__options">
                             <div v-for="item in interactOptions" :key="item.id" class="interact-option"
                                 :class="{ selected: selectedOptionId === item.id }" @click="selectedOptionId = item.id">
@@ -53,12 +53,21 @@
                     </div>
                     <div class="interact-card__ref">
                         <span class="interact-card__ref-label">{{ t("agriFile.referenceImage") }}</span>
-                        <img class="interact-card__ref-img" src="@/assets/img/agricultural/patrol-guide.png" alt="" />
+                        <!-- <img class="interact-card__ref-img" :src="guideImage" alt="" /> -->
+                        <photo-provider :photo-closable="true">
+                            <photo-consumer :src="guideImage">
+                                <img class="interact-card__ref-img" :src="guideImage" alt="" />
+                            </photo-consumer>
+                        </photo-provider>
                     </div>
                 </div>
                 <div class="guide-card">
                     <div class="guide-title">{{ t("agriFile.patrolGuide") }}</div>
-                    <img class="guide-card__img" src="@/assets/img/agricultural/patrol-guide.png" alt="" />
+                    <photo-provider :photo-closable="true">
+                        <photo-consumer :src="guideImage">
+                            <img class="guide-card__img" :src="guideImage" alt="" />
+                        </photo-consumer>
+                    </photo-provider>
                 </div>
             </template>
             <div v-else>
@@ -99,7 +108,7 @@
 </template>
 
 <script setup>
-import { computed, nextTick, onActivated, ref } from "vue";
+import { computed, nextTick, onActivated, onMounted, ref } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { Icon } from 'vant';
 import { ElMessage } from "element-plus";
@@ -108,6 +117,7 @@ import albumUploadPopup from "../components/albumUploadPopup.vue";
 import leaveConfirmPopup from "@/components/popup/leaveConfirmPopup.vue";
 import tipPopup from "@/components/popup/tipPopup.vue";
 import { useI18n } from "@/i18n";
+import { base_img_url2 } from "@/api/config";
 import wx from "weixin-js-sdk";
 
 const { t } = useI18n();
@@ -115,6 +125,7 @@ const route = useRoute();
 const router = useRouter();
 
 const isAbnormal = computed(() => route.query.type === "abnormal");
+const pageTitle = computed(() => route.query.pageName || "巡园要点");
 
 function isTruthyFlag(value) {
     return value === 1 || value === "1" || value === true || value === "true";
@@ -141,17 +152,71 @@ const isFromShare = computed(() => {
     return false;
 });
 
-const theme = computed(() => t("agriFile.interactTheme"));
-const level = computed(() => t("agriFile.riskLevel2"));
-const reason = computed(() => t("agriFile.interactReason"));
-const zoneName = computed(() => t("agriFile.zoneOne"));
-/** 互动选项,后续由接口赋值 */
-const interactOptions = ref([
-    { id: 1, name: "选项一" },
-    { id: 2, name: "选项二" },
-    { id: 3, name: "选项三" },
-]);
+const DEFAULT_GUIDE_IMAGE = require("@/assets/img/agricultural/patrol-guide.png");
+
+function resolveImageUrl(path) {
+    if (!path) return "";
+    const text = String(path).trim();
+    if (!text) return "";
+    if (/^https?:\/\//i.test(text)) return text;
+    return base_img_url2 + text.replace(/^\//, "");
+}
+
+const interactTasks = ref([]);
+const currentTaskIndex = ref(0);
 const selectedOptionId = ref(null);
+
+const currentTask = computed(() => interactTasks.value[currentTaskIndex.value] || null);
+const currentContent = computed(() => currentTask.value?.interact_content || {});
+
+const theme = computed(
+    () => currentContent.value.interact_title || t("agriFile.interactTheme")
+);
+const reason = computed(
+    () => currentContent.value.interact_theme || t("agriFile.interactReason")
+);
+const level = computed(
+    // () => currentTask.value?.weather_risk || t("agriFile.riskLevel2")
+    () => t("agriFile.riskLevel2")
+);
+const zoneName = computed(() => t("agriFile.zoneOne"));
+const assessmentDesc = computed(
+    () => currentContent.value.interact_explain || t("agriFile.agriAssessmentDesc")
+);
+const interactProblem = computed(
+    () => currentContent.value.interact_problem || t("agriFile.interactQuestionText")
+);
+const guideImage = computed(
+    // () => resolveImageUrl(currentContent.value.guide_url) || DEFAULT_GUIDE_IMAGE
+    () => resolveImageUrl('感知_玉米_4_32.png') || DEFAULT_GUIDE_IMAGE
+);
+
+const interactOptions = computed(() => {
+    const options = currentContent.value.interact_options;
+    if (!Array.isArray(options)) return [];
+    return options.map((name, index) => ({
+        id: index + 1,
+        name: String(name),
+    }));
+});
+
+const applyTaskIndex = (index) => {
+    currentTaskIndex.value = index;
+    selectedOptionId.value = null;
+};
+
+const fetchInternalInteractTasks = async () => {
+    try {
+        const res = await VE_API.questionnaire.getInternalInteractTasks({ zone_id: 42 });
+        const tasks = res?.data?.interact_tasks;
+        interactTasks.value = Array.isArray(tasks) ? tasks : [];
+        applyTaskIndex(0);
+    } catch (e) {
+        console.warn("[growthTrack] getInternalInteractTasks failed", e);
+        interactTasks.value = [];
+    }
+};
+
 const showUploadPopup = ref(false);
 const showSuccessPopup = ref(false);
 const showLeavePopup = ref(false);
@@ -163,7 +228,7 @@ const situationList = [
 const handleShare = () => {
     const query = {
         askInfo: { title: "转发巡园要点", content: "是否分享给好友" },
-        shareText: "速长窗口不追肥?后期发育慢人一步,直接掉队!",
+        shareText: reason.value || "速长窗口不追肥?后期发育慢人一步,直接掉队!",
         targetUrl: "growth_track",
         paramsPage: JSON.stringify({
             id: route.query.id,
@@ -182,6 +247,12 @@ const handleSubmit = () => {
         ElMessage.warning(t("agriFile.pleaseSelectOption"));
         return;
     }
+    // 选第二项:切换到下一条内部感知任务
+    const isSecondOption = selectedOptionId.value === 2;
+    if (isSecondOption && currentTaskIndex.value < interactTasks.value.length - 1) {
+        applyTaskIndex(currentTaskIndex.value + 1);
+        return;
+    }
     ElMessage.success(t("agriFile.submitSuccess"));
 };
 
@@ -229,7 +300,16 @@ const confirmLeave = () => {
     router.back();
 };
 
+// onMounted(() => {
+//     if (!isAbnormal.value) {
+//         fetchInternalInteractTasks();
+//     }
+// });
+
 onActivated(() => {
+    if (!isAbnormal.value) {
+        fetchInternalInteractTasks();
+    }
     if (route.query.showUpload !== "1") return;
     nextTick(() => {
         showUploadPopup.value = true;
@@ -310,7 +390,7 @@ onActivated(() => {
             }
 
             &__share {
-                // flex: none;
+                flex: none;
                 display: flex;
                 align-items: center;
                 gap: 4px;

+ 1 - 1
src/views/old_mini/entry_information/components/selectEquipment.vue

@@ -487,7 +487,7 @@ function buildPlotPayload(includeEquipment = true) {
             crop_id: Number(item.categoryId),
             variety_list: [Number(item.id)],
             phenophase: getPhenophaseLabel(item),
-            start_time: item.startTime,
+            start_time: item?.startTime,
             plant_area: Number(item.area),
             point: item.location,
         })),

+ 1 - 1
src/views/old_mini/growth_report/components/PlotDetailContent.vue

@@ -161,7 +161,7 @@ const infoItems = computed(() => [
     {
         key: "startTime",
         label: "起始种植时间",
-        value: props.detail.startTime || "--",
+        value: props.detail?.startTime || "--",
         icon: require("@/assets/img/report/time.png"),
     },
     {