Переглянути джерело

feat:修改病虫显示 页面逻辑样式

wangsisi 2 тижнів тому
батько
коміт
ead151a238

+ 185 - 0
src/components/popup/ImagePreviewPopup.vue

@@ -0,0 +1,185 @@
+<template>
+    <!-- 图片大图预览弹窗:深色遮罩 + Swipe 轮播 + 左右切换 -->
+    <popup
+        v-model:show="showValue"
+        teleport="body"
+        position="center"
+        :close-on-click-overlay="true"
+        :overlay-style="{ backdropFilter: 'blur(4px)' }"
+        class="image-preview-popup"
+    >
+        <div class="image-preview-popup__content" @click="handleClose">
+            <div class="image-preview-popup__swipe-wrap" @click.stop>
+                <Swipe
+                    ref="previewSwipeRef"
+                    v-model="activeIndex"
+                    :show-indicators="false"
+                    :autoplay="0"
+                    :loop="false"
+                    indicator-color="#2199F8"
+                    @change="handleSwipeChange"
+                >
+                    <SwipeItem v-for="(img, index) in imageList" :key="`${img}-${index}`">
+                        <div class="image-preview-popup__slide">
+                            <img class="image-preview-popup__img" :src="img" alt="" />
+                        </div>
+                    </SwipeItem>
+                </Swipe>
+
+                <!-- 左箭头 -->
+                <div
+                    v-if="imageList.length > 1 && activeIndex > 0"
+                    class="image-preview-popup__arrow image-preview-popup__arrow--left"
+                    @click.stop="handlePrev"
+                >
+                    <el-icon size="20">
+                        <ArrowLeftBold color="#F0D09C" />
+                    </el-icon>
+                </div>
+                <!-- 右箭头 -->
+                <div
+                    v-if="imageList.length > 1 && activeIndex < imageList.length - 1"
+                    class="image-preview-popup__arrow image-preview-popup__arrow--right"
+                    @click.stop="handleNext"
+                >
+                    <el-icon size="20">
+                        <ArrowRightBold color="#F0D09C" />
+                    </el-icon>
+                </div>
+            </div>
+        </div>
+    </popup>
+</template>
+
+<script setup>
+import { computed, ref, watch } from 'vue';
+import { Popup, Swipe, SwipeItem } from 'vant';
+import { ArrowLeftBold, ArrowRightBold } from '@element-plus/icons-vue';
+
+const props = defineProps({
+    /** 是否显示弹窗,支持 v-model:show */
+    show: {
+        type: Boolean,
+        default: false,
+    },
+    /** 预览图片列表 */
+    images: {
+        type: Array,
+        default: () => [],
+    },
+    /** 初始展示的图片索引 */
+    startIndex: {
+        type: Number,
+        default: 0,
+    },
+});
+
+const emit = defineEmits(['update:show', 'change']);
+
+const previewSwipeRef = ref(null);
+const activeIndex = ref(0);
+
+/** 弹窗显隐双向绑定 */
+const showValue = computed({
+    get: () => props.show,
+    set: (value) => emit('update:show', value),
+});
+
+/** 过滤后的有效图片列表 */
+const imageList = computed(() => props.images.filter(Boolean));
+
+/** 打开弹窗或 startIndex 变化时,定位到初始索引 */
+watch(
+    () => [props.show, props.startIndex],
+    ([show, start]) => {
+        if (!show) return;
+        const lastIndex = Math.max(imageList.value.length - 1, 0);
+        const normalizedIndex = Math.min(Math.max(start || 0, 0), lastIndex);
+        activeIndex.value = normalizedIndex;
+        previewSwipeRef.value?.swipeTo(activeIndex.value);
+    }
+);
+
+/** Swipe 滑动切换 */
+function handleSwipeChange(index) {
+    activeIndex.value = Number(index) || 0;
+    emit('change', activeIndex.value);
+}
+
+/** 切换到上一张 */
+function handlePrev() {
+    if (!imageList.value.length) return;
+    activeIndex.value = Math.max(activeIndex.value - 1, 0);
+    previewSwipeRef.value?.swipeTo(activeIndex.value);
+    emit('change', activeIndex.value);
+}
+
+/** 切换到下一张 */
+function handleNext() {
+    if (!imageList.value.length) return;
+    const lastIndex = imageList.value.length - 1;
+    activeIndex.value = Math.min(activeIndex.value + 1, lastIndex);
+    previewSwipeRef.value?.swipeTo(activeIndex.value);
+    emit('change', activeIndex.value);
+}
+
+/** 关闭弹窗 */
+function handleClose() {
+    showValue.value = false;
+}
+</script>
+
+<style scoped lang="scss">
+.image-preview-popup {
+    width: 100% !important;
+    max-width: 100% !important;
+
+    &__content {
+        width: 100%;
+        box-sizing: border-box;
+    }
+
+    &__swipe-wrap {
+        position: relative;
+        width: 100%;
+    }
+
+    &__slide {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 100%;
+    }
+
+    &__img {
+        display: block;
+        width: 100%;
+        max-height: 100vh;
+        object-fit: contain;
+        user-select: none;
+    }
+
+    &__arrow {
+        position: absolute;
+        top: 50%;
+        transform: translateY(-50%);
+        width: 40px;
+        height: 40px;
+        border-radius: 50%;
+        background: rgba(0, 0, 0, 0.45);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        z-index: 3;
+        cursor: pointer;
+
+        &--left {
+            left: 8px;
+        }
+
+        &--right {
+            right: 8px;
+        }
+    }
+}
+</style>

+ 16 - 6
src/i18n/recordDetails-messages.js

@@ -6,6 +6,7 @@ export const recordDetailsZh = {
     scienceLabel: "科普知识:",
     phenotypeLabel: "表型特征:",
     highRiskLabel: "高发区域:",
+    harmLevelLabel: "危害等级:",
     farmAnalysisLabel: "农情研判:",
     patrolLabel: "巡园要点:",
     growthQuestion: "果径是否显著小于正常果,且果皮失去光泽、呈现暗哑状态?",
@@ -35,9 +36,13 @@ export const recordDetailsZh = {
     currentStatus: "当前现状:当前{label}进入到{period}",
     uploadPhoto: "点击上传照片",
     confirmInfo: "点击记录巡园照片",
-    abnormalBanner: "发现异常,拍照记录",
-    abnormalBannerDesc: "系统为您智能匹配农事方案",
-    abnormalRecord: "异常记录",
+    abnormalBanner: "点击记录病虫害动态",
+    abnormalBannerDesc: "生成精细处方图",
+    abnormalRecord: "AI 识别",
+    partitionBannerTitle: "物候不整齐?",
+    partitionBannerDesc:
+        "如果区域长势不同,会降低病虫害防治功效,建议根据长势拆分区域,进行分区精细管理,达到减药减肥的目的",
+    partitionManage: "分区管理",
     abnormalCount: "有多少植株出现了异常?",
     inputPlaceholder: "请输入",
     ratioRequired: "请输入占比",
@@ -92,6 +97,7 @@ export const recordDetailsEn = {
     scienceLabel: "Science: ",
     phenotypeLabel: "Phenotype: ",
     highRiskLabel: "High-risk areas: ",
+    harmLevelLabel: "Damage level: ",
     farmAnalysisLabel: "Assessment: ",
     patrolLabel: "Patrol points: ",
     growthQuestion:
@@ -122,9 +128,13 @@ export const recordDetailsEn = {
     currentStatus: "Status: {label} entered {period}",
     uploadPhoto: "Upload photos",
     confirmInfo: "Tap to record patrol photos",
-    abnormalBanner: "Report abnormality with photos",
-    abnormalBannerDesc: "Smart farm work recommendations for you",
-    abnormalRecord: "Abnormal log",
+    abnormalBanner: "Tap to record pest and disease dynamics",
+    abnormalBannerDesc: "Generate precision prescription maps",
+    abnormalRecord: "AI Recognition",
+    partitionBannerTitle: "Uneven phenology?",
+    partitionBannerDesc:
+        "If growth varies by area, pest control is less effective. Split zones by vigor for precision management to reduce pesticide and fertilizer use.",
+    partitionManage: "Zone management",
     abnormalCount: "What % of plants show abnormality?",
     inputPlaceholder: "Enter",
     ratioRequired: "Please enter the ratio",

+ 2 - 1
src/views/old_mini/agri_file/components/remoteSensingChart.vue

@@ -113,6 +113,7 @@ const buildDataZoom = (dates) => {
 };
 
 const toNumber = (value) => {
+    if (value == null || value === "") return null;
     const num = Number(value);
     return Number.isNaN(num) ? null : num;
 };
@@ -203,7 +204,7 @@ const normalizedData = computed(() => {
 const fetchChartData = async () => {
     
     const params = {
-        zone_id: '260',
+        zone_id: '319',
         date: getTodayDateString(),
     };
     loading.value = true;

+ 377 - 193
src/views/old_mini/recordDetails/index.vue

@@ -2,134 +2,96 @@
     <div class="record-wrap">
         <custom-header :name="t('recordDetails.workName')"></custom-header>
         <div class="record-content">
-            <div class="record-header" v-if="recordType === 'growth'">
-                <span>{{ t('agriRecord.growthWorkName') }}</span>
-                <div class="question">{{ currentGrowthAnomalyDetail?.interact_question }}</div>
-            </div>
-            <div class="record-header" v-else-if="recordType === 'pest'">
-                <span>{{ t('agriRecord.pestWorkName') }}</span>
-                <div class="question">{{ currentPestDetail?.interaction_reason }}</div>
-            </div>
-            <div class="record-header" v-else>
-                <span>{{ t('agriRecord.phenologyWorkName') }}</span>
-                <div class="question">{{ workDetail.interaction_issue }}</div>
+            <div class="record-header">
+                <span>{{ t(recordHeaderInfo.titleKey) }}</span>
+                <div class="question">{{ recordHeaderInfo.question }}</div>
             </div>
             <div class="record-body">
-                <div class="card-wrap" v-if="recordType === 'growth'">
-                    <div class="card-item">
-                        <span class="item-label">{{ t('recordDetails.scienceLabel') }}</span>
-                        <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
-                            :collapse-text="expandCollapse.collapse" rows="3"
-                            :content="currentGrowthAnomalyDetail?.agri_judgment" />
-                    </div>
-                    <div class="tabs-list" v-if="!showMap">
-                        <div class="item-tab" :class="{ 'item-tab--active': activeGrowthAnomalyIndex === index }"
-                            v-for="(item, index) in growthAnomalyTabs" :key="index"
-                            @click="handleGrowthAnomalyTabClick(index)">{{ item.label }}
-                        </div>
-                    </div>
-                    <div class="card-item">
-                        <span class="item-label">{{ t('recordDetails.phenotypeLabel') }}</span>
-                        <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
-                            :collapse-text="expandCollapse.collapse" rows="3"
-                            :content="currentGrowthAnomalyDetail?.phenotype" />
-                    </div>
-                    <div class="card-item">
-                        <span class="item-label">{{ t('recordDetails.highRiskLabel') }}</span>
-                        <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
-                            :collapse-text="expandCollapse.collapse" rows="3"
-                            :content="currentGrowthAnomalyDetail?.patrol_points" />
-                    </div>
-                </div>
-                <div class="card-wrap" v-else-if="recordType === 'pest'">
+                <div class="card-wrap">
                     <div class="card-item">
-                        <span class="item-label">{{ t('recordDetails.scienceLabel') }}</span>
-                        <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
-                            :collapse-text="expandCollapse.collapse" rows="3"
-                            :content="currentPestDetail?.kepu" />
+                        <span class="item-label">{{ t(`recordDetails.${leadDetailField.labelKey}`) }}</span>
+                        <text-ellipsis class="item-value" v-bind="textEllipsisBind"
+                            :content="leadDetailField.content" />
                     </div>
-                    <div class="phenology-tip-banner" v-if="recordType !== 'phenology'">
+                    <div v-if="recordType === 'pest'" class="phenology-tip-banner">
                         <div class="banner__left">
                             <div class="banner__title">
-                                <el-icon size="17">
-                                    <WarningFilled />
-                                </el-icon>
                                 <span>{{ t('recordDetails.abnormalBanner') }}</span>
                             </div>
-                            <span class="banner__desc">
-                                {{ t('recordDetails.abnormalBannerDesc') }}
-                            </span>
+                            <span class="banner__desc">{{ t('recordDetails.abnormalBannerDesc') }}</span>
                         </div>
-                        <!-- <div class="banner__btn" @click="goPartitionManage">
-                            分区管理
-                        </div> -->
                         <div class="banner__btn" @click="handleAbnormalRecord">
-                            {{ t('recordDetails.abnormalRecord') }}
+                            <el-icon size="17">
+                                <CameraFilled />
+                            </el-icon>
+                            <span>{{ t('recordDetails.abnormalRecord') }}</span>
                         </div>
                     </div>
-                    <div class="pest-classify-picker">
-                        <div class="pest-classify-picker__row pest-classify-picker__row--top aaa">
-                            <div v-for="(label, i) in pestTopLabels" :key="'top-' + i"
-                                class="pest-classify-picker__top-btn"
-                                :class="{ 'pest-classify-picker__top-btn--active': pestTopIndex === i }"
-                                @click="handlePestTopClick(i)">{{ label }}</div>
+                    <div v-if="recordType === 'growth' && !showMap" class="tabs-list">
+                        <div v-for="(item, index) in growthAnomalyTabs" :key="index" class="item-tab"
+                            :class="{ 'item-tab--active': activeGrowthAnomalyIndex === index }"
+                            @click="handleGrowthAnomalyTabClick(index)">{{ item.label }}</div>
+                    </div>
+                    <template v-if="recordType !== 'pest'">
+                        <div v-for="field in trailDetailFields" :key="field.labelKey" class="card-item">
+                            <span class="item-label">{{ t(`recordDetails.${field.labelKey}`) }}</span>
+                            <text-ellipsis class="item-value" v-bind="textEllipsisBind" :content="field.content" />
+                        </div>
+                        <div v-if="recordType === 'phenology'" class="phenology-tip-banner blue">
+                            <div class="banner__left">
+                                <div class="banner__title">{{ t('recordDetails.partitionBannerTitle') }}</div>
+                                <span class="banner__desc">{{ t('recordDetails.partitionBannerDesc') }}</span>
+                            </div>
+                            <div class="banner__btn" @click="goPartitionManage">
+                                {{ t('recordDetails.partitionManage') }}
+                            </div>
                         </div>
-                        <div class="pest-classify-picker__row pest-classify-picker__row--grid4">
+                    </template>
+                </div>
+                <div v-if="recordType === 'pest'" class="pest-classify-picker">
+                    <div class="pest-classify-picker__tabs" ref="pestTabsRef">
+                        <div v-for="(label, i) in pestTopLabels" :key="'top-' + i" :ref="(el) => setPestTabRef(el, i)"
+                            class="pest-classify-picker__tab"
+                            :class="{ 'pest-classify-picker__tab--active': pestTopIndex === i }"
+                            @click="handlePestTopClick(i)">{{ label }}</div>
+                    </div>
+                    <div class="pest-classify-picker__panel" ref="pestPanelRef"
+                        :style="{ '--arrow-left': `${pestPanelArrowLeft}px` }">
+                        <div class="pest-classify-picker__categories">
                             <div v-for="(item, i) in pestCategoryLabels" :key="'cat-' + item.value"
-                                class="pest-classify-picker__chip pest-classify-picker__chip--solid"
-                                :class="{ 'pest-classify-picker__chip--solid-active': pestCategoryIndex === i }"
+                                class="pest-classify-picker__category"
+                                :class="{ 'pest-classify-picker__category--active': pestCategoryIndex === i }"
                                 @click="handlePestCategoryClick(i)">{{ item.label }}</div>
                         </div>
-                        <div class="pest-classify-picker__row pest-classify-picker__row--grid4 bbb">
+                        <div class="pest-classify-picker__details">
                             <div v-for="(item, i) in pestDetailLabels" :key="'det-' + item.value"
-                                class="pest-classify-picker__chip pest-classify-picker__chip--soft"
-                                :class="{ 'pest-classify-picker__chip--soft-active': pestDetailIndex === i }"
-                                @click="handlePestDetailClick(i)">{{ item.label }}</div>
+                                class="pest-classify-picker__detail"
+                                :class="{ 'pest-classify-picker__detail--active': pestDetailIndex === i }"
+                                @click="handlePestDetailClick(i)">
+                                <span v-if="item.risk_level > 0" class="pest-classify-picker__badge"
+                                    :class="`pest-classify-picker__badge--${item.risk_level}`">
+                                    {{ t('agriRecord.riskLevel', { level: item.risk_level }) }}
+                                </span>
+                                <span class="pest-classify-picker__detail-text">{{ item.label }}</span>
+                            </div>
                         </div>
                     </div>
-                    <div class="card-item">
-                        <span class="item-label">{{ t('recordDetails.phenotypeLabel') }}</span>
-                        <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
-                            :collapse-text="expandCollapse.collapse" rows="3" :content="currentPestDetail?.phenotype" />
-                    </div>
-                    <div class="card-item">
-                        <span class="item-label">{{ t('recordDetails.highRiskLabel') }}</span>
-                        <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
-                            :collapse-text="expandCollapse.collapse" rows="3"
-                            :content="currentPestDetail?.patrol_points" />
-                    </div>
                 </div>
-                <div class="card-wrap" v-else>
-                    <div class="card-item">
-                        <span class="item-label">{{ t('recordDetails.farmAnalysisLabel') }}</span>
-                        <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
-                            :collapse-text="expandCollapse.collapse" rows="3"
-                            :content="workDetail.crop_condition_analysis" />
-                    </div>
-                    <div class="card-item">
-                        <span class="item-label">{{ t('recordDetails.patrolLabel') }}</span>
-                        <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
-                            :collapse-text="expandCollapse.collapse" rows="3"
-                            :content="workDetail.inspection_keypoints" />
-                    </div>
-                    <div class="card-item">
-                        <span class="item-label">{{ t('recordDetails.phenotypeLabel') }}</span>
-                        <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
-                            :collapse-text="expandCollapse.collapse" rows="3"
-                            :content="workDetail.phenotypic_characteristics" />
-                    </div>
-                    <div class="phenology-tip-banner blue" v-if="recordType === 'phenology'">
-                        <div class="banner__left">
-                            <div class="banner__title">{{ $t('物候不整齐?') }}</div>
-                            <span class="banner__desc">
-                                如果区域长势不同,会降低病虫害防治功效,
-                                建议根据长势拆分区域,进行分区精细管理,
-                                达到减药减肥的目的
-                            </span>
-                        </div>
-                        <div class="banner__btn" @click="goPartitionManage">
-                            分区管理
+                <div v-if="recordType === 'pest'" class="card-wrap pest-detail-card">
+                    <div v-for="field in trailDetailFields" :key="field.labelKey" class="card-item"
+                        :class="{ 'card-item--phenotype': field.image }">
+                        <div v-if="field.image" class="card-item__phenotype">
+                            <div class="card-item__phenotype-content">
+                                <span class="item-label">{{ t(`recordDetails.${field.labelKey}`) }}</span>
+                                <text-ellipsis class="item-value" v-bind="textEllipsisBind" :content="field.content" />
+                            </div>
+                            <img class="card-item__phenotype-image" :src="field.image" alt=""
+                                @click="handlePhenotypeImagePreview(field.image)" />
                         </div>
+                        <template v-else>
+                            <span class="item-label">{{ t(`recordDetails.${field.labelKey}`) }}</span>
+                            <text-ellipsis class="item-value" v-bind="textEllipsisBind" :content="field.content" />
+                        </template>
                     </div>
                 </div>
                 <!-- <div class="tabs-list" v-if="showMap">
@@ -164,20 +126,17 @@
                             <div class="current-status">{{ currentStatusText }}</div>
                         </div>
                         <div class="time-line">
-                            <GrowthStageTimeline
-                                :key="phenologyTimelineKey"
-                                v-model="growthStageIndex"
-                                :stages="growthStages"
-                                @scroll-settled="onStageScrollSettled"
-                                @locale-change="getFindPhenologyInfo"
-                            />
+                            <GrowthStageTimeline :key="phenologyTimelineKey" v-model="growthStageIndex"
+                                :stages="growthStages" @scroll-settled="onStageScrollSettled"
+                                @locale-change="getFindPhenologyInfo" />
                         </div>
                         <div class="confirm-btn-wrap">
                             <div class="confirm-btn" @click="hanldeSubmit">{{ t('recordDetails.confirmInfo') }}</div>
                         </div>
                     </div>
                     <div class="phenology-track-section">
-                        <PhenologyTrackTimelineItem ref="phenologyTrackTimelineRef" :abnormalType="recordTypeObj[recordType]" />
+                        <PhenologyTrackTimelineItem ref="phenologyTrackTimelineRef"
+                            :abnormalType="recordTypeObj[recordType]" />
                     </div>
                 </div>
             </div>
@@ -188,6 +147,9 @@
 
         <RiskAssessPopup v-model:show="showRiskAssessPopup" />
 
+        <!-- 表型特征图片大图预览 -->
+        <ImagePreviewPopup v-model:show="showPhenotypeImagePreview" :images="phenotypePreviewImages" />
+
         <!-- 物候 上传弹窗 -->
         <UploadProgressPopup ref="phenologyUploadPopupRef" v-model:show="showPhenologyUploadPopup"
             @cancel="handleCancelPhenologyUploadPopup" :upload-required="false" @confirm="handleConfirmPhenologyUpload">
@@ -203,7 +165,8 @@
         <UploadProgressPopup ref="pestUploadPopupRef" v-model:show="showPestUploadPopup"
             :popup-image-upload-loading="pestPopupImageUploadLoading" :init-img-arr="pestInitImgArr"
             :confirm-text="t('recordDetails.confirmUpload')" :upload-required="false"
-            @reset="handlePestUploadPopupReset" @cancel="handleCancelPestUploadPopup" @confirm="handleConfirmPestUpload">
+            @reset="handlePestUploadPopupReset" @cancel="handleCancelPestUploadPopup"
+            @confirm="handleConfirmPestUpload">
             <template #header>
                 <div class="upload-form">
                     <div class="form-item special-input">
@@ -240,6 +203,7 @@ import customHeader from "@/components/customHeader.vue";
 import PhenologyTrackTimelineItem from "@/components/pageComponents/PhenologyTrackTimelineItem.vue";
 import GrowthStageTimeline from "@/components/pageComponents/GrowthStageTimeline.vue";
 import UploadProgressPopup from "@/components/popup/UploadProgressPopup.vue";
+import ImagePreviewPopup from "@/components/popup/ImagePreviewPopup.vue";
 import RiskAssessPopup from "./components/RiskAssessPopup.vue";
 import { ref, onActivated, watch, nextTick, computed } from 'vue';
 import { useRouter, useRoute } from 'vue-router';
@@ -249,6 +213,16 @@ import { RECORD_KEY_MAP } from '@/i18n/recordTextMap';
 import { TextEllipsis } from "vant";
 import { ElMessage } from "element-plus";
 import IndexMap from "./map/index.js";
+import pestPhenotypePlaceholder from '@/assets/img/common/sd-1.jpg';
+import pestPreviewImg2 from '@/assets/img/common/sd-2.jpg';
+import pestPreviewImg3 from '@/assets/img/common/sd-3.jpg';
+
+/** 表型大图预览 mock 多图(联调后可删) */
+const PHENOTYPE_PREVIEW_MOCK_IMAGES = [
+    pestPhenotypePlaceholder,
+    pestPreviewImg2,
+    pestPreviewImg3,
+];
 
 const router = useRouter();
 const route = useRoute();
@@ -271,12 +245,28 @@ const expandCollapse = computed(() => ({
     collapse: t('recordDetails.collapse'),
 }));
 
+const textEllipsisBind = computed(() => ({
+    expandText: expandCollapse.value.expand,
+    collapseText: expandCollapse.value.collapse,
+    rows: 3,
+}));
+
 function goPartitionManage() {
     router.push({ name: 'MapManage' });
 }
 
 const showMap = ref(false);
 const showRiskAssessPopup = ref(false);
+/** 表型特征图片预览弹窗 */
+const showPhenotypeImagePreview = ref(false);
+const phenotypePreviewImages = ref([]);
+
+/** 点击表型缩略图,打开大图预览 */
+function handlePhenotypeImagePreview(image) {
+    if (!image) return;
+    phenotypePreviewImages.value = PHENOTYPE_PREVIEW_MOCK_IMAGES;
+    showPhenotypeImagePreview.value = true;
+}
 
 /** 病虫害态势监控:分类选择(接入接口后可绑定 workDetail) */
 const pestTopIndex = ref(0);
@@ -309,10 +299,13 @@ const pestCategoryLabels = computed(() => {
     }));
 });
 
+const PEST_MOCK_RISK_LEVELS = [3, 2, 1, 0];
+
 const pestDetailLabels = computed(() =>
-    pestData.value.map((item) => ({
+    pestData.value.map((item, index) => ({
         label: item.label ?? item.name ?? '',
         value: item.value ?? item.fourth_type ?? item.id,
+        risk_level: PEST_MOCK_RISK_LEVELS[index % PEST_MOCK_RISK_LEVELS.length],
     }))
 );
 
@@ -345,6 +338,7 @@ const handlePestTopClick = async (index) => {
     pestCategoryIndex.value = 0;
     pestDetailIndex.value = 0;
     await getAbnormalPlan();
+    syncPestPanelArrow();
 };
 
 const handlePestCategoryClick = async (index) => {
@@ -357,6 +351,37 @@ const handlePestDetailClick = (index) => {
     pestDetailIndex.value = index;
 };
 
+const pestTabsRef = ref(null);
+const pestTabRefs = ref([]);
+const pestPanelRef = ref(null);
+const pestPanelArrowLeft = ref(0);
+
+const setPestTabRef = (el, index) => {
+    if (el) {
+        pestTabRefs.value[index] = el;
+    }
+};
+
+const syncPestPanelArrow = () => {
+    nextTick(() => {
+        requestAnimationFrame(() => {
+            const activeTab = pestTabRefs.value[pestTopIndex.value];
+            const panel = pestPanelRef.value;
+            if (!activeTab || !panel) return;
+            const panelRect = panel.getBoundingClientRect();
+            const tabRect = activeTab.getBoundingClientRect();
+            pestPanelArrowLeft.value = tabRect.left + tabRect.width / 2 - panelRect.left;
+        });
+    });
+};
+
+watch(pestTopIndex, syncPestPanelArrow);
+watch(recordType, (type) => {
+    if (type === 'pest') {
+        syncPestPanelArrow();
+    }
+});
+
 const indexMap = new IndexMap();
 const recordMapContainer = ref(null);
 const uploadMapContainer = ref(null);
@@ -613,6 +638,72 @@ const handleTabClick = (index) => {
 
 const workDetail = ref({});
 
+const recordHeaderInfo = computed(() => {
+    if (recordType.value === 'growth') {
+        return {
+            titleKey: 'agriRecord.growthWorkName',
+            question: currentGrowthAnomalyDetail.value?.interact_question,
+        };
+    }
+    if (recordType.value === 'pest') {
+        return {
+            titleKey: 'agriRecord.pestWorkName',
+            question: currentPestDetail.value?.interaction_reason,
+        };
+    }
+    return {
+        titleKey: 'agriRecord.phenologyWorkName',
+        question: workDetail.value.interaction_issue,
+    };
+});
+
+const leadDetailField = computed(() => {
+    if (recordType.value === 'growth') {
+        return {
+            labelKey: 'scienceLabel',
+            content: currentGrowthAnomalyDetail.value?.agri_judgment,
+        };
+    }
+    if (recordType.value === 'pest') {
+        return {
+            labelKey: 'scienceLabel',
+            content: currentPestDetail.value?.kepu,
+        };
+    }
+    return {
+        labelKey: 'farmAnalysisLabel',
+        content: workDetail.value.crop_condition_analysis,
+    };
+});
+
+const trailDetailFields = computed(() => {
+    if (recordType.value === 'growth') {
+        return [
+            { labelKey: 'phenotypeLabel', content: currentGrowthAnomalyDetail.value?.phenotype },
+            { labelKey: 'highRiskLabel', content: currentGrowthAnomalyDetail.value?.patrol_points },
+        ];
+    }
+    if (recordType.value === 'pest') {
+        const detail = currentPestDetail.value;
+        return [
+            {
+                labelKey: 'harmLevelLabel',
+                content: detail?.harm ?? detail?.harm_desc ?? detail?.harm_level ?? detail?.damage,
+            },
+            {
+                labelKey: 'phenotypeLabel',
+                content: detail?.phenotype,
+                image: pestPhenotypePlaceholder,
+            },
+            { labelKey: 'highRiskLabel', content: detail?.patrol_points },
+        ];
+    }
+    return [
+        { labelKey: 'patrolLabel', content: workDetail.value.inspection_keypoints },
+        { labelKey: 'phenotypeLabel', content: workDetail.value.phenotypic_characteristics },
+    ];
+});
+
 const growthStageIndex = ref();
 const growthStages = ref([]);
 const phenologyRawList = ref([]);
@@ -746,7 +837,8 @@ onActivated(async () => {
         await getWorkDetail();
     }
     if (route.query.type === 'pest') {
-        getAbnormalPlan();
+        await getAbnormalPlan();
+        syncPestPanelArrow();
     }
     if (route.query.type === 'growth') {
         getGrowthAnomalyInfo();
@@ -892,83 +984,29 @@ const getFindPhenologyInfo = async () => {
                     .item-value {
                         display: inline;
                     }
-                }
 
-                .card-item+.card-item {
-                    margin-top: 8px;
-                }
-
-                .pest-classify-picker {
-                    margin: 8px -10px;
-                    padding: 10px;
-                    // border-radius: 8px;
-                    background: #f2f3f5;
-                    display: flex;
-                    flex-direction: column;
-                    align-items: center;
-                    .aaa{
-                        background: #E9E9E9;
-                        width: fit-content;
-                        padding: 3px;
-                        border-radius: 4px;
-                    }
-                    .bbb{
-                        background: #fff;
-                        width: stretch;
-                        padding: 3px;
-                        border-radius: 4px;
-                    }
-
-                    &__row {
+                    &__phenotype {
                         display: flex;
-                        justify-content: center;
-                        gap: 8px;
-                        width: stretch;
-
-                        &--top {
-                            .pest-classify-picker__top-btn {
-                                padding: 6px 30px;
-                                border-radius: 6px;
-                                background: #E9E9E9;
-                                color: #767676;
-                            }
-
-                            .pest-classify-picker__top-btn--active {
-                                background: #ffffff;
-                                color: #1a1a1a;
-                            }
-                        }
+                        align-items: flex-start;
+                        gap: 6px;
 
-                        &--grid4 {
-                            margin-top: 10px;
-                            display: grid;
-                            grid-template-columns: repeat(4, 1fr);
-                            gap: 8px;
+                        &-content {
+                            flex: 1;
+                            min-width: 0;
                         }
                     }
 
-                    &__chip {
-                        border: none;
-                        padding: 6px 4px;
-                        border-radius: 6px;
-                        font-size: 13px;
-                        text-align: center;
-                        white-space: nowrap;
-                        overflow: hidden;
-                        text-overflow: ellipsis;
-                        background: #ffffff;
-                        color: #909090;
-                    }
-
-                    &__chip--solid-active {
-                        background: #2199f8;
-                        color: #ffffff;
+                    &__phenotype-image {
+                        width: 70px;
+                        height: 63px;
+                        object-fit: cover;
+                        border-radius: 5px;
+                        cursor: pointer;
                     }
+                }
 
-                    &__chip--soft-active {
-                        background: rgba(33, 153, 248, 0.14);
-                        color: #2199f8;
-                    }
+                .card-item+.card-item {
+                    margin-top: 8px;
                 }
 
                 .border-wrap {
@@ -1041,6 +1079,149 @@ const getFindPhenologyInfo = async () => {
                 margin-top: 10px;
             }
 
+            .pest-classify-picker {
+                padding: 10px;
+                background: #f2f3f5;
+                border-radius: 8px;
+                display: flex;
+                flex-direction: column;
+                align-items: center;
+
+                &__tabs {
+                    position: relative;
+                    z-index: 2;
+                    display: inline-flex;
+                    width: fit-content;
+                    padding: 3px;
+                    border-radius: 4px;
+                    background: #fff;
+                    box-sizing: border-box;
+                }
+
+                &__tab {
+                    position: relative;
+                    padding: 6px 28px;
+                    border-radius: 4px;
+                    font-size: 13px;
+                    line-height: 18px;
+                    color: #767676;
+                    text-align: center;
+                    box-sizing: border-box;
+
+                    &--active {
+                        background: #2199f8;
+                        color: #fff;
+                    }
+                }
+
+                &__panel {
+                    position: relative;
+                    align-self: stretch;
+                    width: 100%;
+                    margin-top: 10px;
+                    padding: 12px 10px 10px;
+                    border-radius: 4px;
+                    background: #fff;
+                    box-sizing: border-box;
+
+                    &::before {
+                        content: '';
+                        position: absolute;
+                        top: -7px;
+                        left: var(--arrow-left, 50%);
+                        transform: translateX(-50%);
+                        width: 0;
+                        height: 0;
+                        border-left: 8px solid transparent;
+                        border-right: 8px solid transparent;
+                        border-bottom: 8px solid #fff;
+                        z-index: 3;
+                    }
+                }
+
+                &__categories {
+                    display: flex;
+                    justify-content: space-between;
+                    gap: 2px;
+                    margin-bottom: 10px;
+                    overflow-x: auto;
+                    -webkit-overflow-scrolling: touch;
+                }
+
+                &__category {
+                    position: relative;
+                    flex: 1;
+                    padding-bottom: 5px;
+                    font-size: 13px;
+                    color: #909090;
+                    text-align: center;
+
+                    &--active {
+                        color: #333;
+                        border-bottom: 2px solid #3A3A3A;
+                    }
+                }
+
+                &__details {
+                    display: grid;
+                    grid-template-columns: repeat(4, 1fr);
+                    gap: 10px;
+                    padding-top: 6px;
+                }
+
+                &__detail {
+                    position: relative;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    padding: 5px 6px;
+                    border: 1px solid transparent;
+                    border-radius: 4px;
+                    background: #f5f5f5;
+                    color: #909090;
+                    font-size: 12px;
+                    text-align: center;
+                    box-sizing: border-box;
+
+                    &--active {
+                        border-color: #2199f8;
+                        background: rgba(33, 153, 248, 0.1);
+                        color: #2199f8;
+                    }
+                }
+
+                &__detail-text {
+                    display: block;
+                    width: 100%;
+                    overflow: hidden;
+                    text-overflow: ellipsis;
+                    white-space: nowrap;
+                }
+
+                &__badge {
+                    position: absolute;
+                    top: -9px;
+                    right: -8px;
+                    padding: 0 5px;
+                    border-radius: 2px;
+                    font-size: 10px;
+                    color: #fff;
+                    z-index: 1;
+
+                    &--1 {
+                        background: rgba(33, 153, 248, 0.35);
+                    }
+
+                    &--2 {
+                        background: rgba(33, 153, 248, 0.65);
+                    }
+
+                    &--3 {
+                        background: #2199f8;
+                    }
+                }
+            }
+
             .phenology-track-section {
                 margin-top: 15px;
                 display: flex;
@@ -1057,16 +1238,14 @@ const getFindPhenologyInfo = async () => {
         gap: 8px;
         padding: 10px;
         border-radius: 8px;
-        background: linear-gradient(180deg, #FFDFC5 -12.59%, #FFFFFF 38.15%);
-        margin-top: 8px;
+        background: rgba(255, 149, 61, 0.1);
+        border: 1px solid rgba(255, 149, 61, 0.5);
+        margin-top: 10px;
 
         .banner__left {
             flex: 1;
 
             .banner__title {
-                display: flex;
-                align-items: center;
-                gap: 4px;
                 font-size: 16px;
                 font-weight: 600;
                 color: #FF953D;
@@ -1079,18 +1258,23 @@ const getFindPhenologyInfo = async () => {
         }
 
         .banner__btn {
+            display: flex;
+            align-items: center;
+            gap: 4px;
             padding: 7px 16px;
             border-radius: 25px;
             color: #ffffff;
             background: linear-gradient(180deg, #FFB273 0%, #FF953D 100%);
         }
 
-        &.blue{
+        &.blue {
             background: #F1F8FE;
             border: 1px solid rgba(33, 153, 248, 0.5);
+
             .banner__title {
                 color: #2199F8;
             }
+
             .banner__btn {
                 background: linear-gradient(180deg, #8ACBFF 0%, #2199F8 100%);
             }

+ 9 - 9
src/views/old_mini/work_detail/index.vue

@@ -408,15 +408,15 @@ const getContentStatusStyle = (farmData) => {
     const status = Number(farmData?.work_status);// 农事状态
     let background = '#2199F8';
 
-    if (status === 2) {
-        if(type === 3 || type === 4) {
-            background = '#FF943D';
-        } else if(type === 5) {
-            background = '#FF6A6A';
-        } else {
-            background = '#2199F8';
-        }
-    } else if (status === 3) {
+    if(type === 3 || type === 4) {
+        background = '#FF943D';
+    } else if(type === 5) {
+        background = '#FF6A6A';
+    } else {
+        background = '#2199F8';
+    }
+
+    if(status === 3) {
         background = '#C7C7C7';
     }
     return {