소스 검색

feat:修改初次报告样式,对接气象报告

wangsisi 4 일 전
부모
커밋
f0d2998425

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

@@ -96,6 +96,11 @@ module.exports = {
         url: config.base_new_url + "report/query_abnormal_report",
         type: "get",
     },
+    // 气象报告列表
+    weatherReportList: {
+        url: config.base_new_url + "report/weather_report_list",
+        type: "get",
+    },
     // 异常巡检拍照提交
     inspect: {
         url: config.base_new_url + "report/inspect",

BIN
src/assets/img/report/p4.png


BIN
src/assets/img/report/p5.png


BIN
src/assets/img/report/p6.png


+ 126 - 10
src/views/old_mini/agri_file/index.vue

@@ -118,7 +118,28 @@
                         </span> -->
                     </div>
                     <div class="report-card__body">
-                        <div class="report-card__title">{{ item.title }}</div>
+                        <div class="report-card__head">
+                            <div class="report-card__title">{{ item.title }}</div>
+                            <div v-if="item.date" class="report-card__day">{{ item.date }}</div>
+                        </div>
+                        <div class="report-card__desc">{{ item.desc }}</div>
+                    </div>
+                </div>
+                <!-- 气象报告:weather_report_list -->
+                <div
+                    v-for="item in weatherReportList"
+                    :key="item.id"
+                    class="report-card"
+                    @click="goReportDetail(item)"
+                >
+                    <div class="report-card__cover">
+                        <img :src="item.cover" alt="" />
+                    </div>
+                    <div class="report-card__body">
+                        <div class="report-card__head">
+                            <div class="report-card__title">{{ item.title }}</div>
+                            <div v-if="item.date" class="report-card__day">{{ item.date }}</div>
+                        </div>
                         <div class="report-card__desc">{{ item.desc }}</div>
                     </div>
                 </div>
@@ -177,6 +198,7 @@ const INSPECT_SUCCESS_EVENT = "inspect-upload-success";
 
 function onInspectSuccess() {
     fetchReportList();
+    fetchWeatherReportList();
     fetchCropArchiveList();
 }
 
@@ -380,6 +402,8 @@ async function fetchTmpAlbumImages() {
 const reportCover = require("@/assets/img/common/sd-1.jpg");
 /** 诊断报告列表:风险报告 / 胁迫报告 / 异常农情研判报告 */
 const reportList = ref([]);
+/** 气象报告列表:weather_report_list */
+const weatherReportList = ref([]);
 
 function resolveReportCover() {
     return readStoredCropQuestionPic() || reportCover;
@@ -402,10 +426,47 @@ function unwrapApiData(raw) {
     return Array.isArray(res.data) ? res.data[0] : res.data;
 }
 
+/**
+ * risk_report / stress_report:兼容
+ * - 新:{ current_time, reports: [...] }
+ * - 旧:data 为报告对象或数组
+ */
+function unwrapRiskStressEnvelope(raw) {
+    const res = Array.isArray(raw) ? raw[0] : raw;
+    if (Number(res?.code) !== 200 || !res.data) {
+        return { report: null, currentTime: "" };
+    }
+    const data = res.data;
+    if (Array.isArray(data)) {
+        return { report: data[0] || null, currentTime: "" };
+    }
+    if (Array.isArray(data.reports)) {
+        return {
+            report: data.reports[0] || null,
+            currentTime: data.current_time || "",
+        };
+    }
+    return {
+        report: data,
+        currentTime: data.current_time || "",
+    };
+}
+
 function pickReportDate(data) {
     return data?.create_time || data?.update_time || data?.date || data?.report_date || "";
 }
 
+/** 日期字段 → 仅年月日;兼容 2026-09-11 / Wed, 23 Sep 2026 09:42:00 GMT */
+function formatReportDay(value) {
+    if (!value) return "";
+    const text = String(value).trim();
+    const matched = text.match(/(\d{4})-(\d{2})-(\d{2})/);
+    if (matched) return `${matched[1]}-${matched[2]}-${matched[3]}`;
+    const d = new Date(text);
+    if (Number.isNaN(d.getTime())) return "";
+    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
+}
+
 /** 异常研判报告:取 summary 首段正文作卡片简介 */
 function pickAbnormalReportDesc(summary) {
     const text = String(summary || "").trim();
@@ -436,8 +497,8 @@ async function fetchReportList() {
         VE_API.questionnaire.queryAbnormalReport({ tel }).catch(() => null),
     ]);
 
-    const riskData = unwrapApiData(riskRaw);
-    const stressData = unwrapApiData(stressRaw);
+    const { report: riskData, currentTime: riskTime } = unwrapRiskStressEnvelope(riskRaw);
+    const { report: stressData, currentTime: stressTime } = unwrapRiskStressEnvelope(stressRaw);
     // query_abnormal_report:data.reports / data.report 均为报告数组
     const abnormalRes = Array.isArray(abnormalRaw) ? abnormalRaw[0] : abnormalRaw;
     const abnormalRawList =
@@ -457,8 +518,10 @@ async function fetchReportList() {
             title: riskData
                 ? `未来预警: ${entry?.weatherRisk || riskData.risk_name || "预警"}`
                 : "",
-            desc: riskData?.explain_simple || riskData?.interact_explain || "",
-            date: pickReportDate(riskData),
+            // 卡片简介:explain_simple
+            desc: riskData?.explain_simple || "",
+            // 日期:外层 current_time
+            date: formatReportDay(riskTime),
             cover,
         },
         {
@@ -467,8 +530,10 @@ async function fetchReportList() {
             title: stressData
                 ? `当下胁迫: ${entry?.weatherStress || stressData.stress_name || "胁迫"}`
                 : "",
-            desc: stressData?.explain_simple || stressData?.interact_explain || "",
-            date: pickReportDate(stressData),
+            // 卡片简介:explain_simple
+            desc: stressData?.explain_simple || "",
+            // 日期:外层 current_time
+            date: formatReportDay(stressTime),
             cover,
         },
     ].filter((item) => item.title || item.desc);
@@ -481,14 +546,48 @@ async function fetchReportList() {
             type: "abnormal_report",
             title: item.title || "异常农情诊断报告",
             // 卡片简介:analysis[].desc
-            desc:item.explain_simple ||"",
-            cover: item.cover_url || cover,
-            date: pickReportDate(item),
+            desc: item.explain_simple || "",
+            // cover_url 相对路径需拼 base_img_url2
+            cover: resolveCropQuestionImageUrl(item.cover_url) || cover,
+            // 异常报告日期:calc_date → 年月日
+            date: formatReportDay(item.calc_date),
         }));
 
     reportList.value = [...baseList, ...abnormalList];
 }
 
+/** 气象报告列表:weather_report_list → 卡片展示在诊断报告下方 */
+async function fetchWeatherReportList() {
+    const cover = resolveReportCover();
+    try {
+        const raw = await VE_API.questionnaire.weatherReportList({}).catch(() => null);
+        const res = Array.isArray(raw) ? raw[0] : raw;
+        if (Number(res?.code) !== 200) {
+            weatherReportList.value = [];
+            return;
+        }
+        const reports = Array.isArray(res?.data?.reports) ? res.data.reports : [];
+        weatherReportList.value = reports
+            .filter((item) => item && (item.id != null || item.explain_simple || item.risk_name))
+            .map((item) => ({
+                id: `weather-${item.id}`,
+                reportId: item.id,
+                type: "weather",
+                title: item.risk_name
+                    ? `气象报告: ${item.risk_name}`
+                    : item.crop_name
+                      ? `气象报告: ${item.crop_name}`
+                      : "气象报告",
+                desc: item.explain_simple || item.interact_explain || "",
+                date: formatReportDay(item.report_date),
+                // cover_url 相对路径需拼 base_img_url2
+                cover: resolveCropQuestionImageUrl(item.cover_url) || cover,
+            }));
+    } catch {
+        weatherReportList.value = [];
+    }
+}
+
 const showUploadPopup = ref(false);
 const showCompleteFarmPopup = ref(false);
 // 后续由接口控制是否展示代管授权弹窗
@@ -782,6 +881,7 @@ async function loadPatrolContent() {
     await fetchCropQuestionAlbumPic();
     await fetchTmpAlbumImages();
     await fetchReportList();
+    await fetchWeatherReportList();
     await fetchCropArchiveList();
     await nextTick();
     bindZoneOverlays();
@@ -1305,11 +1405,27 @@ watch(activeNav, (key) => {
                 margin-top: 4px;
             }
 
+            &__head {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                gap: 8px;
+            }
+
             &__title {
+                flex: 1;
+                min-width: 0;
                 font-size: 16px;
                 font-weight: 500;
             }
 
+            &__day {
+                flex: none;
+                color: #9E9B9B;
+                font-size: 14px;
+                white-space: nowrap;
+            }
+
             &__desc {
                 margin-top: 4px;
                 color: #9E9B9B;

+ 197 - 7
src/views/old_mini/agri_file/pages/diagnosisReport.vue

@@ -73,8 +73,16 @@
                     :key="`${activeTab}-${sIdx}`"
                     class="report-block"
                 >
-                    <div v-if="section.title" class="report-block__divider">
-                        <span>{{ section.title }}</span>
+                    <div
+                        v-if="section.title"
+                        class="report-block__divider"
+                        :class="{ 'report-block__divider--plain': section.titlePlain }"
+                    >
+                        <span class="report-block__title-text">{{ section.title }}</span>
+                        <span
+                            v-if="section.titleHint"
+                            class="report-block__title-hint"
+                        >{{ section.titleHint }}</span>
                     </div>
 
                     <!-- 导语在图表前(品质潜力) -->
@@ -83,8 +91,31 @@
                         class="report-block__intro"
                     >{{ section.intro }}</p>
 
+                    <!-- 图例(病虫风险等,图片外单独绘制) -->
+                    <ul
+                        v-if="section.legend?.length"
+                        class="report-block__legend"
+                        :class="{ 'report-block__legend--split': section.legendSplit }"
+                    >
+                        <li
+                            v-for="(item, lIdx) in section.legend"
+                            :key="lIdx"
+                            class="report-block__legend-item"
+                        >
+                            <i
+                                class="report-block__legend-dot"
+                                :style="{ background: item.color }"
+                            ></i>
+                            <span>{{ item.label }}</span>
+                        </li>
+                    </ul>
+
                     <!-- 图表占位(后续替换图片) -->
-                    <div v-if="section.showChart && !section.chartAfter" class="report-block__chart">
+                    <div
+                        v-if="section.showChart && !section.chartAfter"
+                        class="report-block__chart"
+                        :class="{ 'report-block__chart--framed': section.chartFramed }"
+                    >
                         <img
                             class="report-block__chart-img"
                             :class="{ 'report-block__chart-img--natural': section.chartNatural }"
@@ -125,11 +156,18 @@
                         </div>
                     </template>
 
-                    <!-- 图表在内容后(农事管理) -->
-                    <div v-if="section.showChart && section.chartAfter" class="report-block__chart report-block__chart--after">
+                    <!-- 图表在内容后(农事管理 / 病虫表格) -->
+                    <div
+                        v-if="section.chartAfterSrc || (section.showChart && section.chartAfter)"
+                        class="report-block__chart report-block__chart--after"
+                        :class="{
+                            'report-block__chart--framed': section.chartFramed,
+                            'report-block__chart--table': section.chartAfterTable,
+                        }"
+                    >
                         <img
                             class="report-block__chart-img"
-                            :src="section.chartSrc || ''"
+                            :src="section.chartAfterSrc || section.chartSrc || ''"
                             alt=""
                         />
                     </div>
@@ -157,6 +195,20 @@ const TABS = [
         paths: ["M13 2L4 14h7l-1 8 9-12h-7l1-8z"],
     },
     {
+        key: "pest",
+        label: "常见病虫风险",
+        paths: [
+            "M8 7l-2-2",
+            "M16 7l2-2",
+            "M12 5v2",
+            "M8.5 10a3.5 3.5 0 0 1 7 0v5a3.5 3.5 0 0 1-7 0v-5z",
+            "M5 12h3",
+            "M16 12h3",
+            "M5 16h3",
+            "M16 16h3",
+        ],
+    },
+    {
         key: "quality",
         label: "品质潜力",
         paths: [
@@ -296,6 +348,62 @@ const SECTION_MAP = {
             chartSrc: require("@/assets/img/report/p3.png"),
         },
     ],
+    pest: [
+        {
+            title: "气象指标10年统计值",
+            titleHint: "(标准化柱状图,柱顶标注原始值)",
+            titlePlain: true,
+            chartFramed: true,
+            showChart: true,
+            chartSrc: require("@/assets/img/report/p4.png"),
+            legendSplit: true,
+            legend: [
+                { color: "linear-gradient(180deg, #41ABFF 0%, #2199F8 100%)", label: "累计降水量(mm)" },
+                { color: "linear-gradient(180deg, #FEC99D 0%, #FF9843 100%)", label: "平均气温(°C)" },
+                { color: "linear-gradient(180deg, #A1D8AB 0%, #71C280 100%)", label: "平均相对湿度(%)" },
+                { color: "linear-gradient(180deg, #FCD993 0%, #FCCB6C 100%)", label: "累计日照时数(h)" },
+                { color: "linear-gradient(180deg, #DB95FF 0%, #D27AFF 100%)", label: "连续干旱时长(d)" },
+                { color: "linear-gradient(180deg, #FAB6BE 0%, #FA9AA6 100%)", label: "连续寡照时长(d)" },
+                { color: "linear-gradient(90deg, #9EDFE2 0%, #6ACFD4 100%)", label: "旱涝急转频次(次)" },
+            ],
+        },
+        {
+            title: "各物候期病虫害风险等级",
+            titleHint: "(直方图,1=低,2=中,3=高)",
+            titlePlain: true,
+            chartFramed: true,
+            showChart: true,
+            chartSrc: require("@/assets/img/report/p5.png"),
+            legend: [
+                { color: "linear-gradient(180deg, #41ABFF 0%, #2199F8 100%)", label: "真菌型病害" },
+                { color: "linear-gradient(180deg, #FEC99D 0%, #FF9843 100%)", label: "蓟马" },
+                { color: "linear-gradient(180deg, #A1D8AB 0%, #71C280 100%)", label: "刺吸式害虫" },
+                { color: "linear-gradient(180deg, #FCD993 0%, #FCCB6C 100%)", label: "细菌型病害" },
+            ],
+            note: "注:因指标量纲不同采用标准化展示,柱顶数字为10年统计原始值。",
+            intro: "病虫害发生与温度、湿度、降雨等气象条件密切相关,同时也受树体抗性影响。金柚各阶段主要病虫风险与关键气象胁迫对应关系的重点分析如下:",
+            type: "list",
+            items: [
+                {
+                    title: "春梢—开花期寡照条件,削弱花果营养积累,增加花期虫害风险",
+                    content:
+                        "春梢—开花期累计日照时数仅为140 h,连续寡照时长达到12天,为全年最低。持续低光照会降低叶片光合产物积累,影响花器发育和坐果基础;同时春季温湿条件适宜时,蓟马等害虫易发生,进一步影响花器质量和坐果稳定性。",
+                },
+                {
+                    title: "坐果—幼果期谢花后干旱与旱涝急转,影响钙硼供应并增加落果风险",
+                    content:
+                        "坐果—幼果期累计降水量达到656 mm,但连续干旱达到12天,旱涝急转频次达到3次,是全年波动最明显阶段。谢花后短期干旱会导致土壤水分不足,降低根系对钙硼等元素的吸收;同时干旱增强叶片蒸腾作用,使钙更多向叶片分配,进一步降低果实获得量。随后旱涝急转过程中,强降雨会造成养分淋失,持续阴湿降低根系活力,在酸性土壤条件下还可能加剧铝毒胁迫,进一步影响养分吸收,增加幼果发育不良和落果风险。",
+                },
+                {
+                    title: "果实膨大—成熟期高温高湿,促进果实病害发生",
+                    content:
+                        "果实膨大期平均气温26.8℃,累计降水量达到804 mm,旱涝急转频次达到2次。高温、多雨和果面长期湿润环境有利于病原菌繁殖传播,风雨造成的机械损伤进一步增加侵染机会,易诱发炭疽病、疫霉褐腐病等真菌病害,影响果实品质和商品性。",
+                },
+            ],
+            chartAfterSrc: require("@/assets/img/report/p6.png"),
+            chartAfterTable: true,
+        },
+    ],
 };
 
 // ---------------------------------------------------------------------------
@@ -418,10 +526,18 @@ const handleShare = () => {
     display: flex;
     gap: 8px;
     padding-bottom: 6px;
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
+    scrollbar-width: none;
+
+    &::-webkit-scrollbar {
+        display: none;
+    }
 
     &__item {
         position: relative;
-        flex: 1;
+        flex: 0 0 auto;
+        min-width: 76px;
         display: flex;
         flex-direction: column;
         align-items: center;
@@ -433,8 +549,11 @@ const handleShare = () => {
         color: #86909c;
         font-size: 14px;
         line-height: 16px;
+        text-align: center;
+        padding: 0 10px;
         box-sizing: border-box;
         border: 1px solid transparent;
+        white-space: nowrap;
 
         &.active {
             background: #fff;
@@ -521,6 +640,65 @@ const handleShare = () => {
             height: 1px;
             background: #e5e6eb;
         }
+
+        &--plain {
+            display: block;
+            font-size: 14px;
+            font-weight: 600;
+            line-height: 22px;
+            color: #1d2129;
+
+            &::before,
+            &::after {
+                display: none;
+            }
+        }
+    }
+
+    &__title-hint {
+        margin-left: 4px;
+        font-size: 13px;
+        font-weight: 400;
+        line-height: 18px;
+        color: #86909c;
+    }
+
+    &__legend {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px 14px;
+        margin: 0 0 10px;
+        padding: 0;
+        list-style: none;
+
+        &--split {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            column-gap: 100px;
+            row-gap: 8px;
+
+            .report-block__legend-dot {
+                width: 20px;
+            }
+        }
+    }
+
+    &__legend-item {
+        display: flex;
+        align-items: center;
+        gap: 5px;
+        min-width: 0;
+        font-size: 12px;
+        line-height: 16px;
+        color: #4e5969;
+    }
+
+    &__legend-dot {
+        display: block;
+        width: 15px;
+        height: 6px;
+        border-radius: 4px;
+        flex-shrink: 0;
     }
 
     &__chart {
@@ -531,6 +709,12 @@ const handleShare = () => {
         -webkit-overflow-scrolling: touch;
         border-radius: 8px;
 
+        &--framed {
+            // border: 1px solid #e5e6eb;
+            background: #fff;
+            padding: 8px 4px 4px;
+        }
+
         &--after {
             margin-top: 10px;
             margin-bottom: 0;
@@ -539,6 +723,12 @@ const handleShare = () => {
                 height: 550px;
             }
         }
+
+        &--table {
+            .report-block__chart-img {
+                height: 410px;
+            }
+        }
     }
 
     &__chart-img {

+ 29 - 0
src/views/old_mini/growth_report/alert_detail.vue

@@ -346,6 +346,10 @@ async function requestReport(apiFn) {
     if (!tel) return null;
     const data = unwrapApiPayload(await apiFn({ tel }));
     if (!data) return null;
+    // 新结构:{ current_time, reports: [...] };旧结构:报告对象 / 数组
+    if (Array.isArray(data.reports) && data.reports.length) {
+        return data.reports[0];
+    }
     return Array.isArray(data) ? data[0] : data;
 }
 
@@ -375,6 +379,21 @@ async function requestAbnormalReport() {
     return reports[0];
 }
 
+/** 气象报告:weather_report_list → data.reports[],按 id 匹配 */
+async function requestWeatherReport() {
+    const data = unwrapApiPayload(await VE_API.questionnaire.weatherReportList({}));
+    if (!data) return null;
+    const reports = Array.isArray(data?.reports) ? data.reports : [];
+    if (!reports.length) return null;
+
+    const targetId = route.query.id;
+    if (targetId !== undefined && targetId !== null && String(targetId) !== "") {
+        const matched = reports.find((item) => String(item?.id) === String(targetId));
+        if (matched) return matched;
+    }
+    return reports[0];
+}
+
 /** 按 detailType 拉取详情 */
 async function fetchAlertDetail() {
     loading.value = true;
@@ -402,6 +421,16 @@ async function fetchAlertDetail() {
             if (report) applyAbnormalReport(report);
             return;
         }
+        if (detailType.value === "weather") {
+            const report = await requestWeatherReport();
+            if (report) {
+                reportTitle.value = report.risk_name
+                    ? `气象报告: ${report.risk_name}`
+                    : "气象报告";
+                applyRiskReport(report);
+            }
+            return;
+        }
         if (detailType.value === "stress") {
             const data = await requestReport(VE_API.questionnaire.stressReport);
             if (data) applyStressReport(data);