2 Commits 9aa979f143 ... 54b5861bba

Autor SHA1 Mensagem Data
  刘秀芳 54b5861bba Merge branch 'agriculture' of http://www.sysuimars.cn:3000/feiniao/feiniao-farm-h5 into agriculture 1 semana atrás
  刘秀芳 d885a5fb15 fix: 对接接口 1 semana atrás

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

@@ -86,4 +86,9 @@ module.exports = {
         url: config.base_new_url + "questionnaire/weather_stress_report",
         type: "get",
     },
+    // 上传图片
+    uploadImages: {
+        url: config.base_new_url + "questionnaire/upload_image",
+        type: "post",
+    },
 }

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

@@ -5,7 +5,7 @@
         teleport="body"
         :z-index="20000"
         class="complete-farm-popup"
-        :close-on-click-overlay="true"
+        :close-on-click-overlay="false"
     >
         <div class="complete-farm-popup__title">{{ t("agriFile.completeFarmInfo") }}</div>
         <div class="complete-farm-popup__subtitle">

+ 3 - 2
src/components/popup/farmCalendarPopup.vue

@@ -4,7 +4,8 @@
         round
         teleport="body"
         class="farm-calendar-popup"
-        :close-on-click-overlay="true"
+        :z-index="20000"
+        :close-on-click-overlay="false"
     >
         <div class="farm-calendar-popup__title">{{ t("agriFile.completeFarmInfo") }}</div>
         <div class="farm-calendar-popup__subtitle">
@@ -109,7 +110,7 @@ const handleOpen = () => {
                 content: "";
                 position: absolute;
                 left: 50%;
-                bottom: -6px;
+                bottom: -5px;
                 transform: translateX(-50%);
                 border-left: 6px solid transparent;
                 border-right: 6px solid transparent;

+ 2 - 3
src/views/old_mini/agri_file/components/albumUploadPopup.vue

@@ -28,8 +28,8 @@
                 <el-date-picker
                     v-model="uploadDate"
                     type="date"
-                    format="YYYY.MM.DD"
-                    value-format="YYYY.MM.DD"
+                    format="YYYY-MM-DD"
+                    value-format="YYYY-MM-DD"
                     placeholder="请选择上传日期"
                     :editable="false"
                     style="width: 100%"
@@ -187,7 +187,6 @@ const handleConfirm = () => {
         description: description.value.trim(),
         date: uploadDate.value,
     });
-    // 正式上传接口未定时,先关闭弹窗由父组件处理
     confirming.value = false;
     showValue.value = false;
 };

+ 49 - 15
src/views/old_mini/agri_file/pages/growthTrack.vue

@@ -127,6 +127,8 @@ const router = useRouter();
 const isAbnormal = computed(() => route.query.type === "abnormal");
 const pageTitle = computed(() => route.query.pageName || "巡园要点");
 
+const id = computed(() => route.query.id);
+
 function isTruthyFlag(value) {
     return value === 1 || value === "1" || value === true || value === "true";
 }
@@ -207,7 +209,7 @@ const applyTaskIndex = (index) => {
 
 const fetchInternalInteractTasks = async () => {
     try {
-        const res = await VE_API.questionnaire.getInternalInteractTasks({ zone_id: 42 });
+        const res = await VE_API.questionnaire.getInternalInteractTasks({ zone_id: id.value });
         const tasks = res?.data?.interact_tasks;
         interactTasks.value = Array.isArray(tasks) ? tasks : [];
         applyTaskIndex(0);
@@ -231,7 +233,7 @@ const handleShare = () => {
         shareText: reason.value || "速长窗口不追肥?后期发育慢人一步,直接掉队!",
         targetUrl: "growth_track",
         paramsPage: JSON.stringify({
-            id: route.query.id,
+            id: id.value,
             type: route.query.type,
             fromShare: 1,
         }),
@@ -260,21 +262,53 @@ const needDraw = ref(true);
 
 /** 先跳小程序校验定位;通过后由小程序直接打开勾画/病虫害/上传目标页 */
 const handleUploadClick = () => {
-    const query = [
-        `needDraw=${needDraw.value ? 1 : 0}`,
-        `isAbnormal=${isAbnormal.value ? 1 : 0}`,
-        route.query.id ? `id=${encodeURIComponent(String(route.query.id))}` : "",
-        route.query.type ? `type=${encodeURIComponent(String(route.query.type))}` : "",
-    ]
-        .filter(Boolean)
-        .join("&");
-    wx.miniProgram.navigateTo({
-        url: `/pages/subPages/farm_location_check/index?${query}`,
-    });
+    
+    showUploadPopup.value = true;
+    // const query = [
+    //     `needDraw=${needDraw.value ? 1 : 0}`,
+    //     `isAbnormal=${isAbnormal.value ? 1 : 0}`,
+    //     route.query.id ? `id=${encodeURIComponent(String(route.query.id))}` : "",
+    //     route.query.type ? `type=${encodeURIComponent(String(route.query.type))}` : "",
+    // ]
+    //     .filter(Boolean)
+    //     .join("&");
+    // wx.miniProgram.navigateTo({
+    //     url: `/pages/subPages/farm_location_check/index?${query}`,
+    // });
+};
+
+const getSelectedFarm = () => {
+    try {
+        return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
+    } catch {
+        return {};
+    }
 };
 
-const handleUploadConfirm = () => {
-    showSuccessPopup.value = true;
+const handleUploadConfirm = async ({ images, date } = {}) => {
+    const cloud_url = Array.isArray(images) ? images : [];
+    if (!cloud_url.length) {
+        ElMessage.warning("请先上传照片");
+        return;
+    }
+    const farm = getSelectedFarm();
+    try {
+        const res = await VE_API.questionnaire.uploadImages({
+            cloud_url,
+            farm_id: farm.farm_id ?? farm.id ?? "",
+            zone_id: id.value || "",
+            category_code: farm.category_code || "",
+            zone_name: farm.variety_name || "",
+            create_time: date,
+        });
+        if (res?.code === 200) {
+            showSuccessPopup.value = true;
+            return;
+        }
+        ElMessage.error(res?.msg || "上传失败,请稍后再试");
+    } catch {
+        ElMessage.error("上传失败,请稍后再试");
+    }
 };
 
 const handleComplete = () => {

+ 146 - 81
src/views/old_mini/growth_report/alert_detail.vue

@@ -1,19 +1,19 @@
 <template>
     <div class="alert-detail-page">
         <custom-header :name="pageTitle" />
-        <div class="alert-detail-body">
+        <div class="alert-detail-body" v-loading="loading">
             <!-- 综合研判 -->
-            <div class="section-card">
+            <div v-if="analysis.summary || analysis.crop.name" class="section-card">
                 <div class="section-card__title">综合研判</div>
-                <div class="section-card__desc">{{ analysis.summary }}</div>
+                <div v-if="analysis.summary" class="section-card__desc">{{ analysis.summary }}</div>
 
-                <div class="crop-panel">
+                <div v-if="analysis.crop.name || analysis.crop.metrics.length" class="crop-panel">
                     <div class="crop-panel__head">
                         <img class="crop-panel__cover" :src="analysis.crop.cover" alt="" />
                         <span class="crop-panel__name">{{ analysis.crop.name }}</span>
                         <span class="crop-panel__tag">当前作物</span>
                     </div>
-                    <div class="crop-panel__list">
+                    <div v-if="analysis.crop.metrics.length" class="crop-panel__list">
                         <div
                             v-for="item in analysis.crop.metrics"
                             :key="item.label"
@@ -30,7 +30,7 @@
             </div>
 
             <!-- 巡园结果与分析 -->
-            <div class="section-card">
+            <div v-if="patrolResults.length" class="section-card">
                 <div class="section-card__title">巡园结果与分析</div>
                 <div class="patrol-list">
                     <div
@@ -38,7 +38,7 @@
                         :key="item.id"
                         class="patrol-item"
                     >
-                        <img class="patrol-item__cover" :src="item.cover" alt="" />
+                        <img v-if="item.cover" class="patrol-item__cover" :src="item.cover" alt="" />
                         <div class="patrol-item__body">
                             <div class="patrol-item__title">{{ item.title }}</div>
                             <div class="patrol-item__desc">{{ item.desc }}</div>
@@ -48,7 +48,7 @@
             </div>
 
             <!-- 诊断研判分析 -->
-            <div class="section-card">
+            <div v-if="diagnosisList.length" class="section-card">
                 <div class="section-card__title">诊断研判分析</div>
                 <div class="diag-list">
                     <div
@@ -56,15 +56,15 @@
                         :key="item.id"
                         class="diag-item"
                     >
-                        <span class="diag-item__tag">{{ item.tag }}</span>
-                        <div class="diag-item__title">{{ item.title }}</div>
-                        <div class="diag-item__desc">{{ item.desc }}</div>
+                        <span v-if="item.tag" class="diag-item__tag">{{ item.tag }}</span>
+                        <div v-if="item.title" class="diag-item__title">{{ item.title }}</div>
+                        <div v-if="item.desc" class="diag-item__desc">{{ item.desc }}</div>
                     </div>
                 </div>
             </div>
 
             <!-- 农事建议 -->
-            <div class="section-card">
+            <div v-if="adviceList.length" class="section-card">
                 <div class="section-card__title">农事建议</div>
                 <div class="advice-list">
                     <div
@@ -82,92 +82,155 @@
 </template>
 
 <script setup>
-import { computed, ref } from "vue";
+import { computed, onMounted, ref } from "vue";
 import { useRoute } from "vue-router";
 import customHeader from "@/components/customHeader.vue";
 
 const route = useRoute();
 
-const pageTitle = computed(() => route.query.title || "具体预警/具体胁迫");
+const QUERY_PARAMS = {
+    farm_id: "321",
+    zone_id: "46",
+    date: "2026-08-11",
+};
+const DETAIL_TYPE = {
+    WARNING: "warning",
+    STRESS: "stress",
+};
 
 const phenologyIcon = require("@/assets/img/common/header-icon-3.png");
 const weatherIcon = require("@/assets/img/common/header-icon-1.png");
 const soilIcon = require("@/assets/img/common/header-icon-2.png");
 const cropCover = require("@/assets/img/home/plant.png");
-const patrolCover = require("@/assets/img/common/tp-3.png");
 
-/** 综合研判 */
+const detailType = computed(() =>
+    String(route.query.type || "") === DETAIL_TYPE.STRESS
+        ? DETAIL_TYPE.STRESS
+        : DETAIL_TYPE.WARNING
+);
+const detailTitle = ref("");
+const pageTitle = computed(
+    () => detailTitle.value || route.query.title || "具体预警/具体胁迫"
+);
+
 const analysis = ref({
-    summary:
-        "综合巡园照片、当前物候、近期气象和土壤条件判断,本次异常以持续高温干旱造成的水分失衡为主,已表现为嫩梢萎蔫、卷曲和局部灼伤,并伴有浅层根区干燥",
+    summary: "",
     crop: {
-        name: "荔枝",
+        name: "",
         cover: cropCover,
-        metrics: [
-            {
-                label: "当前物候",
-                value: "秋梢新叶速长—红黄叶旺长期",
-                icon: phenologyIcon,
-            },
-            {
-                label: "气象预警",
-                value: "连续3天最高温36-38°C,近7天基本无有效降雨",
-                icon: weatherIcon,
-            },
-            {
-                label: "土壤类型",
-                value: "酸性沙土,树盘及浅层根区明显偏干",
-                icon: soilIcon,
-            },
-        ],
+        metrics: [],
     },
 });
+const patrolResults = ref([]);
+const diagnosisList = ref([]);
+const adviceList = ref([]);
+const loading = ref(false);
+
+/** 接口可能返回对象或数组,统一取第一条 */
+const pickFirstRecord = (res) => {
+    const data = res?.data ?? res;
+    if (Array.isArray(data)) return data[0] || null;
+    if (data && typeof data === "object") return data;
+    return null;
+};
+
+const buildMetrics = (record, type) => {
+    const isStress = type === DETAIL_TYPE.STRESS;
+    return [
+        { label: "当前物候", value: record.phenophase_desc || "", icon: phenologyIcon },
+        {
+            label: isStress ? "气象胁迫" : "气象预警",
+            value: (isStress ? record.stress_desc : record.risk_desc) || "",
+            icon: weatherIcon,
+        },
+        { label: "土壤类型", value: record.soil_desc || "", icon: soilIcon },
+    ].filter((item) => item.value);
+};
+
+/** 农事内容按「1. 标题。正文」拆成建议列表 */
+const parseFarmworkAdvice = (farmworkInfo) => {
+    if (!farmworkInfo || typeof farmworkInfo !== "object") return [];
+    const fallbackTitle = farmworkInfo["农事名称"] || "";
+    const content = String(farmworkInfo["内容信息"] || "").trim();
+    if (!content && !fallbackTitle) return [];
+
+    const blocks = content
+        .split(/\n(?=\d+\.\s*)/)
+        .map((s) => s.trim())
+        .filter(Boolean);
+
+    if (!blocks.length) {
+        return [{ id: 1, title: fallbackTitle, desc: content }];
+    }
 
-/** 巡园结果与分析 */
-const patrolResults = ref([
-    {
-        id: 1,
-        title: "嫩梢萎蔫与灼伤",
-        cover: patrolCover,
-        desc: "最嫩叶出现轻度皱缩、展开不完全,个别嫩梢顶部出现干尖。成熟叶未见明显连续叶缘焦枯,老叶也未见明显脉间黄化。",
-    },
-    {
-        id: 2,
-        title: "嫩梢萎蔫与灼伤",
-        cover: patrolCover,
-        desc: "最嫩叶出现轻度皱缩、展开不完全,个别嫩梢顶部出现干尖。成熟叶未见明显连续叶缘焦枯,老叶也未见明显脉间黄化。",
-    },
-]);
-
-/** 诊断研判分析 */
-const diagnosisList = ref([
-    {
-        id: 1,
-        tag: "表观影响",
-        title: "浅层吸收根已经受到干旱影响,但尚无腐烂迹象",
-        desc: "本物候期尚无花、果器官,研判重点放在叶和梢。高温、强光叠加缺水后,嫩叶失水速度超过根系补水速度,首先表现",
-    },
-    {
-        id: 2,
-        tag: "表观影响",
-        title: "浅层吸收根已经受到干旱影响,但尚无腐烂迹象",
-        desc: "本物候期尚无花、果器官,研判重点放在叶和梢。高温、强光叠加缺水后,嫩叶失水速度超过根系补水速度,首先表现",
-    },
-]);
-
-/** 农事建议 */
-const adviceList = ref([
-    {
-        id: 1,
-        title: "优先恢复根区水分",
-        desc: "本物候期尚无花、果器官,研判重点放在叶和梢。高温、强光叠加缺水后,嫩叶失水速度超过根系补水速度,首先表现",
-    },
-    {
-        id: 2,
-        title: "暂不建议立即大剂量追肥",
-        desc: "本物候期尚无花、果器官,研判重点放在叶和梢。高温、强光叠加缺水后,嫩叶失水速度超过根系补水速度,首先表现",
-    },
-]);
+    return blocks.map((block, index) => {
+        const text = block.replace(/^\d+\.\s*/, "");
+        const sep = text.indexOf("。");
+        if (sep > 0 && sep < 40) {
+            return {
+                id: index + 1,
+                title: text.slice(0, sep),
+                desc: text.slice(sep + 1).trim(),
+            };
+        }
+        return {
+            id: index + 1,
+            title: fallbackTitle || `建议${index + 1}`,
+            desc: text,
+        };
+    });
+};
+
+const applyRecord = (record, type) => {
+    if (!record) return;
+    const isStress = type === DETAIL_TYPE.STRESS;
+    detailTitle.value = isStress ? record.stress_name || "" : record.risk_name || "";
+
+    analysis.value = {
+        summary: record.interact_explain || "",
+        crop: {
+            name: record.crop_name || "",
+            cover: cropCover,
+            metrics: buildMetrics(record, type),
+        },
+    };
+
+    diagnosisList.value = [
+        {
+            id: record.id || 1,
+            tag: (isStress ? record.stress_type : record.risk_type) || "",
+            title: (isStress ? record.stress_name : record.risk_name) || "",
+            desc:
+                (isStress
+                    ? record.importance_desc || record.explain_simple
+                    : record.explain_simple) || "",
+        },
+    ].filter((item) => item.title || item.desc);
+
+    adviceList.value = parseFarmworkAdvice(record.farmwork_info);
+    patrolResults.value = [];
+};
+
+const fetchDetail = async () => {
+    const type = detailType.value;
+    const request =
+        type === DETAIL_TYPE.STRESS
+            ? VE_API.questionnaire.getWeatherStress
+            : VE_API.questionnaire.getWeatherRisk;
+    loading.value = true;
+    try {
+        const res = await request(QUERY_PARAMS);
+        applyRecord(pickFirstRecord(res), type);
+    } catch {
+        // 详情页保持空态
+    } finally {
+        loading.value = false;
+    }
+};
+
+onMounted(() => {
+    fetchDetail();
+});
 </script>
 
 <style lang="scss" scoped>
@@ -205,6 +268,7 @@ const adviceList = ref([
         word-break: break-all;
         text-align: justify;
         font-weight: 350;
+        white-space: pre-line;
     }
 }
 
@@ -402,6 +466,7 @@ const adviceList = ref([
         line-height: 20px;
         color: #4E5969;
         word-break: break-all;
+        white-space: pre-line;
     }
 }
 </style>

+ 67 - 8
src/views/old_mini/growth_report/components/CropAlertPanel.vue

@@ -110,6 +110,15 @@ const store = useStore();
 const { t } = useI18n();
 
 const GUIDE_STORAGE_KEY = "GROWTH_REPORT_CROP_SWITCH_GUIDE";
+const QUERY_PARAMS = {
+    farm_id: "321",
+    zone_id: "46",
+    date: "2026-08-11",
+};
+const CARD_ID = {
+    WARNING: "warning",
+    STRESS: 2,
+};
 
 const props = defineProps({
     cropName: {
@@ -249,23 +258,72 @@ watch(
     }
 );
 
-const alertCards = [
+const alertCards = ref([
     {
-        id: "warning",
-        title: "具体预警",
+        id: CARD_ID.WARNING,
+        source: "warning",
+        title: "",
         icon: weatherIcon,
-        content: "详情体预警体预警体预警体预警体预警体预详情详情详情详情详情详情详情详情详情详情",
+        content: "",
         patrolTip: "",
     },
     {
-        id: 2,
+        id: CARD_ID.STRESS,
+        source: "stress",
         type: "abnormal",
-        title: "某某胁迫",
+        title: "",
         icon: weatherIcon,
-        content: "详情详情详情具体预警具体预警情详情详详情详情详情详情详情详情",
+        content: "",
         patrolTip: "异常态势巡园要点",
     },
-];
+]);
+
+/** 接口可能返回对象或数组,统一取第一条 */
+const pickFirstRecord = (res) => {
+    const data = res?.data ?? res;
+    if (Array.isArray(data)) return data[0] || null;
+    if (data && typeof data === "object") return data;
+    return null;
+};
+
+const patchAlertCard = (cardId, patch) => {
+    const card = alertCards.value.find((item) => item.id === cardId);
+    if (!card) return;
+    Object.assign(card, patch);
+};
+
+const applyWeatherRisk = (record) => {
+    if (!record) return;
+    patchAlertCard(CARD_ID.WARNING, {
+        title: record.risk_name || "",
+        content: record.explain_simple || "",
+    });
+};
+
+const applyWeatherStress = (record) => {
+    if (!record) return;
+    if (record.crop_name) currentCropName.value = record.crop_name;
+    patchAlertCard(CARD_ID.STRESS, {
+        title: record.stress_name || "",
+        content: record.explain_simple || "",
+    });
+};
+
+const fetchAlertCards = async () => {
+    const [riskRes, stressRes] = await Promise.allSettled([
+        VE_API.questionnaire.getWeatherRisk(QUERY_PARAMS),
+        VE_API.questionnaire.getWeatherStress(QUERY_PARAMS),
+    ]);
+
+    if (riskRes.status === "fulfilled") {
+        applyWeatherRisk(pickFirstRecord(riskRes.value));
+    }
+    if (stressRes.status === "fulfilled") {
+        applyWeatherStress(pickFirstRecord(stressRes.value));
+    }
+
+    nextTick(() => syncMidAnchor());
+};
 
 const handleViewDetail = (item) => {
     emit("viewDetail", item);
@@ -287,6 +345,7 @@ const handleHeightChange = ({ height: nextHeight }) => {
 };
 
 onMounted(() => {
+    fetchAlertCards();
     nextTick(() => {
         tabBarHeight.value = resolveTabBarHeight();
         // 等布局完成后再量一次,避免首屏高度偏小;小程序里 tabBar 可能稍晚才量到

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

@@ -463,6 +463,7 @@ const handleViewDetail = (item) => {
         path: "/alert_detail",
         query: {
             title: item?.title || "具体预警",
+            type: item?.source || "warning",
         },
     });
 };