Parcourir la source

feat:添加农事档案页面

wangsisi il y a 1 semaine
Parent
commit
4de92d21d2

+ 158 - 0
src/components/pageComponents/PhenologyTrackTimelineItem copy.vue

@@ -0,0 +1,158 @@
+<template>
+    <template v-if="imageList.length">
+        <div class="phenology-track-item" v-for="item in imageList" :key="item.id">
+            <div class="track-axis">
+                <span class="track-date-side">{{ item?.latest_time?.slice(5, 10) }}</span>
+                <div class="track-line-wrap">
+                    <span class="track-dot"></span>
+                    <div class="track-line"></div>
+                </div>
+            </div>
+            <div class="track-card">
+                <div class="track-card-inner">
+                    <span class="track-badge">{{ item?.latest_time?.slice(5, 10) }}</span>
+                    <span class="track-content">{{ item.zone_name }}</span>
+                </div>
+                <div class="track-images">
+                    <img v-for="(src, i) in item.images" :key="i" class="track-thumb" :src="src.cloud_url" alt="" />
+                </div>
+            </div>
+        </div>
+    </template>
+    <div v-else-if="dataLoaded" class="track-empty">{{ t('agriFile.noData') }}</div>
+</template>
+
+<script setup>
+import { onActivated, ref } from "vue";
+import { useI18n } from "@/i18n";
+
+const { t } = useI18n();
+
+const props = defineProps({
+    abnormalType: {
+        type: [String, Number],
+        default: 1,
+    },
+});
+
+const imageList = ref([]);
+const dataLoaded = ref(false);
+
+const getFarmImages = async () => {
+    const farmData = JSON.parse(localStorage.getItem('selectedFarmData'));
+    const params = {
+        farm_id: farmData.farm_id,
+        variety_code: farmData.farm_variety,
+        abnormal_type: props.abnormalType,
+        record_id: 21,
+    };
+    try {
+        const res = await VE_API.record.farmImages(params);
+        if (res.code === 200) {
+            imageList.value = res.data.zones || [];
+        }
+    } finally {
+        dataLoaded.value = true;
+    }
+};
+
+onActivated(() => {
+    getFarmImages();
+});
+
+defineExpose({
+    refresh: getFarmImages,
+});
+</script>
+
+<style scoped lang="scss">
+.track-empty {
+    padding: 24px 0;
+    text-align: center;
+    font-size: 14px;
+    color: rgba(60, 60, 60, 0.45);
+}
+
+/* Grid:左侧时间轴整列与右侧卡片等高,竖线随卡片高度自适应 */
+.phenology-track-item {
+    display: grid;
+    grid-template-columns: auto 1fr;
+    gap: 10px;
+    align-items: stretch;
+
+    .track-axis {
+        display: flex;
+        flex-direction: column;
+
+        .track-date-side {
+            color: #7e7e7e;
+        }
+
+        .track-line-wrap {
+            display: flex;
+            flex: 1;
+            flex-direction: column;
+            align-items: center;
+            margin-top: 3px;
+
+            .track-dot {
+                width: 6px;
+                height: 6px;
+                border-radius: 50%;
+                background: rgba(29, 33, 41, 0.2);
+                z-index: 1;
+            }
+
+            .track-line {
+                flex: 1;
+                margin-top: 2px;
+                border-left: 1px dashed rgba(29, 33, 41, 0.2);
+            }
+        }
+    }
+
+    .track-card {
+        flex: 1;
+        padding: 10px;
+        border-radius: 6px;
+        background: #f5f5f5;
+
+        .track-card-inner {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            padding: 5px 8px;
+            border-radius: 5px;
+            background: #ffffff;
+
+            .track-badge {
+                padding: 2px 4px;
+                border-radius: 2px;
+                font-size: 13px;
+                color: #ffffff;
+                background: #2199f8;
+                min-width: 42px;
+                box-sizing: border-box;
+            }
+
+            .track-content {
+                color: rgba(60, 60, 60, 0.5);
+            }
+        }
+
+        .track-images {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 12px;
+            margin-top: 10px;
+
+            .track-thumb {
+                width: 56px;
+                height: 56px;
+                border-radius: 8px;
+                object-fit: cover;
+            }
+        }
+    }
+}
+</style>

+ 100 - 53
src/components/pageComponents/PhenologyTrackTimelineItem.vue

@@ -1,29 +1,26 @@
 <template>
-    <template v-if="imageList.length">
-        <div class="phenology-track-item" v-for="item in imageList" :key="item.id">
+    <template v-if="groupedList.length">
+        <div class="phenology-track-item" v-for="group in groupedList" :key="group.date">
             <div class="track-axis">
-                <span class="track-date-side">{{ item?.latest_time?.slice(5, 10) }}</span>
+                <span class="track-date-side">{{ group.date }}</span>
                 <div class="track-line-wrap">
                     <span class="track-dot"></span>
                     <div class="track-line"></div>
                 </div>
             </div>
-            <div class="track-card">
-                <div class="track-card-inner">
-                    <span class="track-badge">{{ item?.latest_time?.slice(5, 10) }}</span>
-                    <span class="track-content">{{ item.zone_name }}</span>
-                </div>
-                <div class="track-images">
-                    <img v-for="(src, i) in item.images" :key="i" class="track-thumb" :src="src.cloud_url" alt="" />
+            <div class="track-cards">
+                <div class="track-card" v-for="item in group.items" :key="item.id">
+                    <span v-if="item.zone_name" class="track-badge">{{ item.zone_name }}</span>
+                    <div v-if="item.content" class="track-content" v-html="highlightContent(item.content)"></div>
                 </div>
             </div>
         </div>
     </template>
-    <div v-else-if="dataLoaded" class="track-empty">{{ t('agriFile.noData') }}</div>
+    <div v-else-if="dataLoaded" class="track-empty">{{ t("agriFile.noData") }}</div>
 </template>
 
 <script setup>
-import { onActivated, ref } from "vue";
+import { computed, onActivated, ref, watch } from "vue";
 import { useI18n } from "@/i18n";
 
 const { t } = useI18n();
@@ -33,13 +30,66 @@ const props = defineProps({
         type: [String, Number],
         default: 1,
     },
+    list: {
+        type: Array,
+        default: null,
+    },
 });
 
 const imageList = ref([]);
 const dataLoaded = ref(false);
 
+const sourceList = computed(() => (Array.isArray(props.list) ? props.list : imageList.value));
+
+function formatDate(value) {
+    if (!value) return "";
+    const text = String(value);
+    const iso = text.match(/(\d{4})-(\d{2})-(\d{2})/);
+    if (iso) return `${iso[2]}/${iso[3]}`;
+    const slash = text.match(/(\d{1,2})\/(\d{1,2})/);
+    if (slash) return `${slash[1].padStart(2, "0")}/${slash[2].padStart(2, "0")}`;
+    if (text.length >= 10) return `${text.slice(5, 7)}/${text.slice(8, 10)}`;
+    return text;
+}
+
+function normalizeItem(item, index) {
+    return {
+        id: item.id ?? index,
+        date: formatDate(item.date || item.latest_time),
+        zone_name: item.zone_name || item.zoneName || "",
+        content: item.content || item.desc || item.text || "",
+    };
+}
+
+const groupedList = computed(() => {
+    const groups = [];
+    const indexMap = new Map();
+    sourceList.value.forEach((item, index) => {
+        const normalized = normalizeItem(item, index);
+        if (!normalized.date) return;
+        if (!indexMap.has(normalized.date)) {
+            indexMap.set(normalized.date, groups.length);
+            groups.push({ date: normalized.date, items: [] });
+        }
+        groups[indexMap.get(normalized.date)].items.push(normalized);
+    });
+    return groups;
+});
+
+function highlightContent(text) {
+    const safe = String(text || "")
+        .replace(/&/g, "&amp;")
+        .replace(/</g, "&lt;")
+        .replace(/>/g, "&gt;");
+    return safe.replace(/(\d+(?:\.\d+)?%|异常|abnormality|abnormal)/gi, '<span class="track-hl">$1</span>');
+}
+
 const getFarmImages = async () => {
-    const farmData = JSON.parse(localStorage.getItem('selectedFarmData'));
+    if (Array.isArray(props.list)) {
+        dataLoaded.value = true;
+        return;
+    }
+    const farmData = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
     const params = {
         farm_id: farmData.farm_id,
         variety_code: farmData.farm_variety,
@@ -56,6 +106,14 @@ const getFarmImages = async () => {
     }
 };
 
+watch(
+    () => props.list,
+    () => {
+        if (Array.isArray(props.list)) dataLoaded.value = true;
+    },
+    { immediate: true },
+);
+
 onActivated(() => {
     getFarmImages();
 });
@@ -73,7 +131,6 @@ defineExpose({
     color: rgba(60, 60, 60, 0.45);
 }
 
-/* Grid:左侧时间轴整列与右侧卡片等高,竖线随卡片高度自适应 */
 .phenology-track-item {
     display: grid;
     grid-template-columns: auto 1fr;
@@ -85,7 +142,10 @@ defineExpose({
         flex-direction: column;
 
         .track-date-side {
-            color: #7e7e7e;
+            padding: 2px 6px;
+            border-radius: 2px;
+            background: #F6F6F6;
+            font-size: 12px;
         }
 
         .track-line-wrap {
@@ -93,7 +153,7 @@ defineExpose({
             flex: 1;
             flex-direction: column;
             align-items: center;
-            margin-top: 3px;
+            margin: 4px 0 8px 0;
 
             .track-dot {
                 width: 6px;
@@ -105,52 +165,39 @@ defineExpose({
 
             .track-line {
                 flex: 1;
+                min-height: 12px;
                 margin-top: 2px;
                 border-left: 1px dashed rgba(29, 33, 41, 0.2);
             }
         }
     }
 
-    .track-card {
-        flex: 1;
-        padding: 10px;
-        border-radius: 6px;
-        background: #f5f5f5;
-
-        .track-card-inner {
-            display: flex;
-            align-items: center;
-            gap: 8px;
-            padding: 5px 8px;
-            border-radius: 5px;
-            background: #ffffff;
-
-            .track-badge {
-                padding: 2px 4px;
-                border-radius: 2px;
-                font-size: 13px;
-                color: #ffffff;
-                background: #2199f8;
-                min-width: 42px;
-                box-sizing: border-box;
-            }
+    .track-cards {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+        padding-bottom: 8px;
+    }
 
-            .track-content {
-                color: rgba(60, 60, 60, 0.5);
-            }
+    .track-card {
+        padding: 6px 5px;
+        border-radius: 4px;
+        background: rgba(245, 245, 245, 0.87);
+
+        .track-badge {
+            display: inline-block;
+            padding: 0 5px;
+            margin-bottom: 6px;
+            border-radius: 2px;
+            background: #2199f8;
+            color: #fff;
         }
 
-        .track-images {
-            display: flex;
-            flex-wrap: wrap;
-            gap: 12px;
-            margin-top: 10px;
-
-            .track-thumb {
-                width: 56px;
-                height: 56px;
-                border-radius: 8px;
-                object-fit: cover;
+        .track-content {
+            color: rgba(60, 60, 60, 0.5);
+
+            :deep(.track-hl) {
+                color: #2199f8;
             }
         }
     }

+ 38 - 0
src/i18n/messages.js

@@ -133,8 +133,23 @@ export default {
             farmLocation: "位置位置位置位置",
             moreFarms: "更多农场",
             patrolRecord: "巡园记录",
+            patrolGrowthTitle: "长势跟踪巡园要点 ({zone})",
+            patrolAbnormalTitle: "异常态势巡园要点 ({zone})",
+            patrolSubject: "某某主题",
+            patrolIssue: "互动问题互动问题互动问题互动问题互",
+            forward: "转发",
+            workName: "农事名称",
+            interactTheme: "互动主题",
+            interactReason: "互动原因互动原因",
+            agriAssessment: "农情研判",
+            agriAssessmentDesc: "互动原因互动原因互动原因互动原因互气象风险等互动原因互动原因互动原因互动原因互气象风险等",
+            uploadPhoto: "上传照片",
             agriAlbum: "农情相册",
             diagnosisReport: "诊断报告",
+            reportUpdate: "更新",
+            reportTitle: "标题",
+            reportDesc: "主题主题主题主题主题主题主题主题主题主题主题主题主题主题主题主题",
+            reportDate: "8月30日",
             addZone: "新增分区",
             addAlbum: "新增相册",
             selectZoneHint: "请勾选您的分区区域",
@@ -163,6 +178,10 @@ export default {
             tabAbnormal: "异常记录",
             tabFarming: "农事记录",
             tabAgriRecord: "农情记录",
+            cropArchive: "作物档案",
+            yearLabel: "{year}年",
+            cropArchiveMetric: "某某指标已来 20% 指标",
+            cropArchiveAbnormal: "发现了 *** 异常",
             tabRemoteSensing: "时序遥感指标",
             remoteSensingChartTitle: "时序遥感指数",
             remoteSensingChartTitleNdvi: "NDVI",
@@ -470,8 +489,23 @@ export default {
             farmLocation: "Location",
             moreFarms: "More farms",
             patrolRecord: "Patrol records",
+            patrolGrowthTitle: "Growth tracking patrol ({zone})",
+            patrolAbnormalTitle: "Abnormality patrol ({zone})",
+            patrolSubject: "Subject",
+            patrolIssue: "Interaction questions interaction questions",
+            forward: "Forward",
+            workName: "Farm work name",
+            interactTheme: "Interaction theme",
+            interactReason: "Interaction reason",
+            agriAssessment: "Agri assessment",
+            agriAssessmentDesc: "Interaction reason interaction reason meteorological risk, etc.",
+            uploadPhoto: "Upload photo",
             agriAlbum: "Agri album",
             diagnosisReport: "Diagnosis report",
+            reportUpdate: "Update",
+            reportTitle: "Title",
+            reportDesc: "Theme theme theme theme theme theme theme theme theme theme theme theme",
+            reportDate: "Aug 30",
             addZone: "Add zone",
             addAlbum: "Add album",
             selectZoneHint: "Please select your zone area",
@@ -500,6 +534,10 @@ export default {
             tabAbnormal: "Abnormalities",
             tabFarming: "Farm Work",
             tabAgriRecord: "Crop Records",
+            cropArchive: "Crop archive",
+            yearLabel: "{year}",
+            cropArchiveMetric: "Some metric has reached 20% of the target",
+            cropArchiveAbnormal: "Found *** abnormality",
             tabRemoteSensing: "Time-series Remote Sensing",
             remoteSensingChartTitle: "Time-series Remote Sensing Index",
             remoteSensingChartTitleNdvi: "NDVI",

+ 7 - 0
src/router/globalRoutes.js

@@ -191,4 +191,11 @@ export default [
         meta: { keepAlive: true },
         component: () => import("@/views/old_mini/agri_file/pages/albumMap.vue"),
     },
+    // 长势跟踪记录
+    {
+        path: "/growth_track",
+        name: "GrowthTrack",
+        meta: { keepAlive: true },
+        component: () => import("@/views/old_mini/agri_file/pages/growthTrack.vue"),
+    },
 ];

+ 0 - 377
src/views/old_mini/agri_file/components/fileFloat.vue

@@ -1,377 +0,0 @@
-<template>
-    <!-- <div class="add-btn">{{ t('点击新建管理分区') }}</div> -->
-    <floating-panel class="file-float-panel" :class="{ 'custom-panel': height === anchors[0] }" v-model:height="height"
-        :anchors="anchors">
-        <div class="file-float-content">
-            <div class="float-tabs">
-                <div class="tab-active-bg" :style="primaryActiveBgStyle"></div>
-                <div v-for="(item, index) in floatTabLabels" :key="item.value" class="tab-item"
-                    @click="changePrimaryTab(index)" :class="{ 'tab-item-active': activeTab === index }">
-                    {{ item.title }}
-                </div>
-            </div>
-            <div class="tab-content-group" v-show="height !== anchors[0]">
-                <template v-if="isAgriRecordTab">
-                    <div class="float-sub-tabs">
-                        <div v-for="(item, index) in agriSubTabLabels" :key="item.value" class="sub-tab-item"
-                            :class="{ 'sub-tab-item-active': activeSubTab === index }" @click="changeSubTab(index)">
-                            {{ item.title }}
-                        </div>
-                    </div>
-                    <div class="tab-loading" v-if="loading">{{ t('agriFile.loading') }}</div>
-                    <div class="tab-empty" v-else-if="displayList.length === 0">{{ t('agriFile.noData') }}</div>
-                    <div
-                        v-else
-                        v-for="item in displayList"
-                        :key="`${activeSubTabValue}-${item.id}`"
-                        class="tab-content-item"
-                    >
-                        <div class="time-tag">{{ item.time }}</div>
-                        <div class="item-info">
-                            {{ item.recordText }}
-                            <span class="blue-text">{{ item.ratio }}{{ item.showRatio ? '%' : '' }}</span>
-                        </div>
-                    </div>
-                </template>
-                <div v-else-if="isRemoteSensingTab" class="remote-sensing-chart">
-                    <div class="remote-sensing-chart__legend">
-                        <div
-                            v-for="item in remoteSensingLegendItems"
-                            :key="item.key"
-                            class="remote-sensing-chart__legend-item"
-                        >
-                            <span
-                                v-if="item.iconType === 'bar'"
-                                class="remote-sensing-chart__legend-bar"
-                                :style="{ background: item.color }"
-                            ></span>
-                            <span
-                                v-else-if="item.iconType === 'dashed'"
-                                class="remote-sensing-chart__legend-line remote-sensing-chart__legend-line--dashed"
-                                :style="{ '--legend-color': item.color }"
-                            ></span>
-                            <span
-                                v-else
-                                class="remote-sensing-chart__legend-line"
-                                :style="{ background: item.color }"
-                            ></span>
-                            <span class="remote-sensing-chart__legend-text">{{ item.label }}</span>
-                        </div>
-                    </div>
-                    <div class="tab-loading" v-if="loading">{{ t('agriFile.loading') }}</div>
-                    <remote-sensing-chart v-else />
-                </div>
-            </div>
-        </div>
-    </floating-panel>
-</template>
-
-<script setup>
-import { useI18n } from "@/i18n";
-import { RECORD_KEY_MAP } from "@/i18n/recordTextMap";
-import { FloatingPanel } from 'vant';
-import { computed, ref } from 'vue';
-import remoteSensingChart from './remoteSensingChart.vue';
-
-const { t } = useI18n();
-
-const props = defineProps({
-    farmRecordData: {
-        type: Object,
-        default: () => ({}),
-    },
-    activeTab: {
-        type: Number,
-        default: 0,
-    },
-    activeSubTab: {
-        type: Number,
-        default: 0,
-    },
-    loading: {
-        type: Boolean,
-        default: false,
-    },
-    cropVariety: {
-        type: String,
-        default: "",
-    },
-});
-
-const emit = defineEmits(["update:activeTab", "update:activeSubTab"]);
-
-const anchors = [
-    130,
-    Math.round(0.45 * window.innerHeight),
-    Math.round(0.8 * window.innerHeight),
-];
-const height = ref(anchors[0]);
-
-const AGRI_SUB_TAB_KEYS = ["phenology", "farming", "abnormal"];
-
-const resolveRecordI18nKey = (record) => {
-    if (record == null || record === "") return "";
-    const text = String(record).trim();
-    return RECORD_KEY_MAP[text] || text;
-};
-
-const floatTabLabels = computed(() => [
-    { title: t("agriFile.tabAgriRecord"), value: "agriRecord" },
-    { title: t("agriFile.tabRemoteSensing"), value: "remoteSensing" },
-]);
-
-const agriSubTabLabels = computed(() => [
-    { title: t("agriFile.tabPhenology"), value: "phenology" },
-    { title: t("agriFile.tabFarming"), value: "farming" },
-    { title: t("agriFile.tabAbnormal"), value: "abnormal" },
-]);
-
-const isAgriRecordTab = computed(() => floatTabLabels.value[props.activeTab]?.value === "agriRecord");
-
-const isRemoteSensingTab = computed(() => floatTabLabels.value[props.activeTab]?.value === "remoteSensing");
-
-const REMOTE_SENSING_LEGEND_ITEMS = [
-    { key: "ndwi", labelKey: "agriFile.remoteSensingLegendNdwi", color: "#6277FB", iconType: "line" },
-    { key: "ndvi", labelKey: "agriFile.remoteSensingLegendNdvi", color: "#1CC277", iconType: "line" },
-    { key: "precipitation", labelKey: "agriFile.remoteSensingLegendPrecipitation", color: "#E2F1FD", iconType: "bar" },
-    { key: "avgPrecipitation", labelKey: "agriFile.remoteSensingLegendAvgPrecipitation", color: "#66BBFF", iconType: "dashed" },
-];
-
-const remoteSensingLegendItems = computed(() =>
-    REMOTE_SENSING_LEGEND_ITEMS.map(({ key, labelKey, color, iconType }) => ({
-        key,
-        label: t(labelKey),
-        color,
-        iconType,
-    }))
-);
-
-const activeSubTabValue = computed(
-    () => agriSubTabLabels.value[props.activeSubTab]?.value || AGRI_SUB_TAB_KEYS[0]
-);
-
-const displayList = computed(() => {
-    if (!isAgriRecordTab.value) return [];
-
-    const list = props.farmRecordData?.[activeSubTabValue.value] || [];
-    return list
-        .map((item) => {
-            const i18nKey = resolveRecordI18nKey(item?.record);
-            return {
-                ...item,
-                recordText: i18nKey ? t(i18nKey) : "",
-                showRatio:
-                    activeSubTabValue.value !== "farming" &&
-                    String(item.ratio ?? "").length > 0,
-            };
-        })
-        .filter((item) => item.recordText.length > 0);
-});
-
-const changePrimaryTab = (index) => {
-    emit("update:activeTab", index);
-};
-
-const changeSubTab = (index) => {
-    emit("update:activeSubTab", index);
-};
-
-const primaryActiveBgStyle = computed(() => ({
-    transform: `translateX(${props.activeTab * 100}%)`,
-}));
-
-</script>
-
-<style lang="scss" scoped>
-.add-btn {
-    position: fixed;
-    top: 50%;
-    left: 50%;
-    transform: translate(-50%, -50%);
-    color: #fff;
-    border-radius: 20px;
-    padding: 0 20px;
-    background: #2199f8;
-    height: 40px;
-    line-height: 40px;
-    cursor: pointer;
-}
-
-.file-float-panel {
-    left: 12px;
-    width: calc(100% - 24px);
-
-    &.custom-panel {
-        background: transparent;
-
-        ::v-deep {
-            .van-floating-panel__header {
-                background: #fff;
-                border-radius: 10px 10px 0 0;
-            }
-
-            .van-floating-panel__content {
-                background: transparent;
-                margin-top: -1px;
-            }
-        }
-    }
-}
-
-.file-float-content {
-    padding: 0 10px 10px;
-    background: #fff;
-    border-radius: 0 0 10px 10px;
-
-    .float-tabs {
-        position: relative;
-        border-radius: 4px;
-        padding: 3px;
-        background: #E9E9E9;
-        display: grid;
-        grid-template-columns: repeat(2, minmax(0, 1fr));
-        align-items: center;
-        overflow: hidden;
-
-        .tab-active-bg {
-            position: absolute;
-            top: 3px;
-            left: 3px;
-            width: calc((100% - 6px) / 2);
-            height: 26px;
-            border-radius: 4px;
-            background: #fff;
-            transition: transform 0.25s ease;
-        }
-
-        .tab-item {
-            position: relative;
-            z-index: 1;
-            flex: 1;
-            height: 26px;
-            line-height: 26px;
-            text-align: center;
-            color: #767676;
-            border-radius: 4px;
-            transition: color 0.2s ease;
-
-            &.tab-item-active {
-                color: #0D0D0D;
-            }
-        }
-    }
-
-    .float-sub-tabs {
-        display: flex;
-        align-items: center;
-        gap: 8px;
-        margin-bottom: 10px;
-
-        .sub-tab-item {
-            height: 26px;
-            line-height: 24px;
-            padding: 0 10px;
-            font-size: 12px;
-            color: #767676;
-            background: #e9e9e9;
-            border-radius: 4px;
-            border: 1px solid transparent;
-            box-sizing: border-box;
-            cursor: pointer;
-            transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
-
-            &.sub-tab-item-active {
-                color: #2199f8;
-                background: #fff;
-                border-color: #2199f8;
-            }
-        }
-    }
-
-    .tab-content-group {
-        padding-top: 12px;
-
-        .tab-loading,
-        .tab-empty {
-            text-align: center;
-            color: #9a9a9a;
-            font-size: 13px;
-            padding: 14px 0;
-        }
-
-        .tab-content-item+.tab-content-item {
-            margin-top: 10px;
-        }
-
-        .tab-content-item {
-            display: flex;
-            align-items: center;
-            gap: 10px;
-
-            .time-tag {
-                color: #2199F8;
-                background: rgba(33, 153, 248, 0.1);
-                font-size: 12px;
-                height: 21px;
-                line-height: 21px;
-                padding: 0 6px;
-                min-width: fit-content;
-                box-sizing: border-box;
-            }
-
-            .item-info {
-                color: rgba(60, 60, 60, 0.5);
-                line-height: 21px;
-            }
-
-            .blue-text {
-                color: #2199f8;
-            }
-        }
-
-        .remote-sensing-chart {
-            &__legend {
-                display: flex;
-                align-items: center;
-                justify-content: space-between;
-                gap: 10px;
-                margin-bottom: 8px;
-            }
-
-            &__legend-item {
-                display: flex;
-                align-items: center;
-                gap: 4px;
-            }
-
-            &__legend-line {
-                width: 15px;
-                height: 3px;
-                border-radius: 2px;
-
-                &--dashed {
-                    background: repeating-linear-gradient(
-                        to right,
-                        var(--legend-color) 0,
-                        var(--legend-color) 5px,
-                        transparent 5px,
-                        transparent 7px
-                    ) !important;
-                }
-            }
-
-            &__legend-bar {
-                width: 8px;
-                height: 12px;
-                border-radius: 1px;
-                flex-shrink: 0;
-            }
-
-            &__legend-text {
-                font-size: 12px;
-                color: #666666;
-            }
-        }
-    }
-}
-</style>

+ 390 - 4
src/views/old_mini/agri_file/index.vue

@@ -22,7 +22,53 @@
                 <span class="nav-card__label">{{ t(item.labelKey) }}</span>
             </div>
         </div>
-        <div v-show="activeNav === 'album'" class="content-panel">
+        <div class="content-panel">
+        <div v-show="activeNav === 'patrol'">
+            <div
+                v-for="item in patrolList"
+                :key="item.id"
+                class="tracking-item"
+                :class="`tracking-item--${item.theme}`"
+                @click="goGrowthTrack(item)"
+            >
+                <div class="tracking-item__header">
+                    <div class="tracking-item__header-row">
+                        <div class="tracking-item__header-left">
+                            <span class="tracking-item__level">
+                                <el-icon class="tracking-item__level-icon"><Bell /></el-icon>
+                                {{ item.level }}
+                            </span>
+                            <span class="tracking-item__title van-ellipsis">{{ item.title }}</span>
+                        </div>
+                        <div class="tracking-item__share" @click.stop>
+                            <el-icon><Right /></el-icon>
+                            <span>{{ t("agriFile.forward") }}</span>
+                        </div>
+                    </div>
+                </div>
+                <div class="tracking-item__body">
+                    <div class="tracking-item__action">
+                        <div class="tracking-item__action-main">
+                            <div class="tracking-item__icon">
+                                <img :src="item.icon" alt="" />
+                                <span class="tracking-item__reason">{{ item.subject }}</span>
+                            </div>
+                            <div class="tracking-item__issue">{{ item.issue }}</div>
+                        </div>
+                        <div class="tracking-item__btn" @click.stop="openUploadPopup">
+                            <el-icon><Plus /></el-icon>
+                            <span>{{ t("agriFile.uploadPhoto") }}</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="crop-archive">
+                <div class="crop-archive__title">{{ t("agriFile.cropArchive") }}</div>
+                <div class="crop-archive__year">{{ t("agriFile.yearLabel", { year: 2025 }) }}</div>
+                <PhenologyTrackTimelineItem :list="cropArchiveList" />
+            </div>
+        </div>
+        <div v-show="activeNav === 'album'">
             <div class="map-card" @click="goAlbumMap">
                 <div ref="mapContainer" class="map-container"></div>
                 <div class="add-zone-btn" @click.stop="goAddZone">
@@ -62,6 +108,24 @@
                 </div>
             </div>
         </div>
+        <div v-show="activeNav === 'report'">
+            <div v-for="item in reportList" :key="item.id" class="report-card">
+                <div class="report-card__cover">
+                    <img :src="item.cover" alt="" />
+                    <span class="report-card__date">{{ item.date }}</span>
+                    <span class="report-card__status">
+                        <i class="report-card__dot"></i>
+                        {{ t("agriFile.reportUpdate") }}
+                    </span>
+                </div>
+                <div class="report-card__body">
+                    <div class="report-card__title">{{ item.title }}</div>
+                    <div class="report-card__desc">{{ item.desc }}</div>
+                </div>
+            </div>
+        </div>
+        </div>
+        <album-upload-popup v-model:show="showUploadPopup" />
     </div>
 </template>
 
@@ -69,11 +133,13 @@
 import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue";
 import { useStore } from "vuex";
 import { useRouter } from "vue-router";
-import { Switch, Plus } from "@element-plus/icons-vue";
+import { Switch, Plus, Bell, Right } from "@element-plus/icons-vue";
 import Overlay from "ol/Overlay";
 import { useI18n } from "@/i18n";
 import FileMap from "./fileMap";
 import * as util from "@/common/ol_common.js";
+import albumUploadPopup from "./components/albumUploadPopup.vue";
+import PhenologyTrackTimelineItem from "@/components/pageComponents/PhenologyTrackTimelineItem.vue";
 
 const { t } = useI18n();
 const store = useStore();
@@ -87,7 +153,7 @@ const defaultAvatar = require("@/assets/img/home/banner.png");
 const farmName = ref("");
 const farmLocation = ref("");
 const circleUrl = ref(defaultAvatar);
-const activeNav = ref("album");
+const activeNav = ref("patrol");
 const mapContainer = ref(null);
 const fileMap = new FileMap();
 const markerEls = {};
@@ -124,6 +190,41 @@ const albumList = ref([
     { id: 3, name: "", count: 125, cover: defaultCover },
 ]);
 
+const reportCover = require("@/assets/img/common/sd-1.jpg");
+const reportList = ref([
+    { id: 1, title: "", desc: "", date: "", cover: reportCover },
+    { id: 2, title: "", desc: "", date: "", cover: require("@/assets/img/common/sd-2.jpg") },
+    { id: 3, title: "", desc: "", date: "", cover: require("@/assets/img/common/sd-3.jpg") },
+]);
+
+const showUploadPopup = ref(false);
+const patrolList = ref([
+    {
+        id: 1,
+        theme: "blue",
+        level: "",
+        title: "",
+        subject: "",
+        issue: "",
+        icon: require("@/assets/img/report/yc-icon.png"),
+    },
+    {
+        id: 2,
+        theme: "blue",
+        level: "",
+        title: "",
+        subject: "",
+        issue: "",
+        icon: require("@/assets/img/report/bh-icon.png"),
+    },
+]);
+
+const cropArchiveList = ref([
+    { id: 1, date: "2025-04-18", zone_name: "", content: "" },
+    { id: 2, date: "2025-04-18", zone_name: "", content: "" },
+    { id: 3, date: "2025-04-10", zone_name: "", content: "" },
+]);
+
 const setMarkerEl = (id, el) => {
     if (el) markerEls[id] = el;
     else delete markerEls[id];
@@ -183,6 +284,24 @@ const fillMockLabels = () => {
     const albumName = t("agriFile.zoneOne");
     zoneList.value = zoneList.value.map((item) => ({ ...item, name: zoneName }));
     albumList.value = albumList.value.map((item) => ({ ...item, name: albumName }));
+    reportList.value = reportList.value.map((item) => ({
+        ...item,
+        title: t("agriFile.reportTitle"),
+        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"),
+    }));
+    cropArchiveList.value = cropArchiveList.value.map((item, index) => ({
+        ...item,
+        zone_name: albumName,
+        content: t(index % 2 === 0 ? "agriFile.cropArchiveMetric" : "agriFile.cropArchiveAbnormal"),
+    }));
 };
 
 const loadFarmInfo = () => {
@@ -206,6 +325,17 @@ const goAlbumMap = () => {
     router.push("/album_map");
 };
 
+const goGrowthTrack = (item) => {
+    router.push({
+        path: "/growth_track",
+        query: { id: item?.id },
+    });
+};
+
+const openUploadPopup = () => {
+    showUploadPopup.value = true;
+};
+
 onMounted(() => {
     fillMockLabels();
     loadFarmInfo();
@@ -275,7 +405,7 @@ watch(activeNav, (key) => {
         display: flex;
         justify-content: space-around;
         padding: 10px 10px;
-        margin-bottom: 6px;
+        margin-bottom: 0;
 
         .nav-card {
             position: relative;
@@ -316,6 +446,7 @@ watch(activeNav, (key) => {
                     border-left: 6px solid transparent;
                     border-right: 6px solid transparent;
                     border-top: 6px solid #2199F8;
+                    z-index: 3;
                 }
             }
         }
@@ -488,5 +619,260 @@ watch(activeNav, (key) => {
             }
         }
     }
+
+    .report-card {
+        overflow: hidden;
+        margin-bottom: 12px;
+        border-radius: 12px;
+        background: #fff;
+
+        &:last-child {
+            margin-bottom: 0;
+        }
+
+        &__cover {
+            position: relative;
+            width: 100%;
+            height: 148px;
+
+            img {
+                display: block;
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+            }
+        }
+
+        &__date {
+            position: absolute;
+            top: 8px;
+            left: 8px;
+            padding: 2px 8px;
+            border-radius: 4px;
+            background: rgba(0, 0, 0, 0.45);
+            color: #fff;
+            font-size: 12px;
+            line-height: 18px;
+        }
+
+        &__status {
+            position: absolute;
+            top: 8px;
+            right: 8px;
+            display: flex;
+            align-items: center;
+            gap: 4px;
+            padding: 2px 8px 2px 6px;
+            border-radius: 20px;
+            background: #FF4D4F;
+            color: #fff;
+            font-size: 12px;
+            line-height: 18px;
+        }
+
+        &__dot {
+            width: 6px;
+            height: 6px;
+            border-radius: 50%;
+            background: #fff;
+        }
+
+        &__body {
+            padding: 10px 12px 12px;
+        }
+
+        &__title {
+            color: #1F1F1F;
+            font-size: 16px;
+            font-weight: 600;
+            line-height: 22px;
+        }
+
+        &__desc {
+            margin-top: 6px;
+            color: rgba(0, 0, 0, 0.45);
+            font-size: 13px;
+            line-height: 20px;
+        }
+    }
+
+    .tracking-item {
+        width: 100%;
+        margin-bottom: 12px;
+        border: 1px solid #fff;
+        border-radius: 10px;
+        box-shadow: 0 4px 4px 0 var(--tracking-shadow);
+        cursor: pointer;
+
+        &:last-of-type {
+            margin-bottom: 0;
+        }
+
+        &--blue {
+            --tracking-header-bg: linear-gradient(90deg, #5BB8FF 0%, #2199F8 100%);
+            --tracking-primary: #2199f8;
+            --tracking-action-border: rgba(33, 153, 248, 0.2);
+            --tracking-shadow: #2199f81a;
+        }
+
+        &__header {
+            position: relative;
+            padding: 10px 10px 18px;
+            border-radius: 10px 10px 0 0;
+            background: var(--tracking-header-bg);
+        }
+
+        &__header-row {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            gap: 8px;
+        }
+
+        &__header-left {
+            display: flex;
+            align-items: center;
+            min-width: 0;
+            gap: 6px;
+        }
+
+        &__level {
+            display: flex;
+            align-items: center;
+            flex-shrink: 0;
+            gap: 2px;
+            padding: 1px 8px;
+            border-radius: 20px;
+            background: #fff;
+            color: #FF6A6A;
+            font-size: 12px;
+        }
+
+        &__level-icon {
+            font-size: 12px;
+        }
+
+        &__title {
+            min-width: 0;
+            color: #fff;
+            font-size: 15px;
+            font-weight: 500;
+        }
+
+        &__share {
+            display: flex;
+            align-items: center;
+            flex-shrink: 0;
+            gap: 2px;
+            padding: 3px 10px;
+            border-radius: 16px;
+            background: #fff;
+            color: #2199F8;
+            font-size: 12px;
+        }
+
+        &__body {
+            position: relative;
+            padding: 14px 10px 10px;
+            margin-top: -8px;
+            border-radius: 10px;
+            background: #fff;
+
+            &::before {
+                content: "";
+                position: absolute;
+                top: -7px;
+                left: 50%;
+                width: 0;
+                height: 0;
+                border-left: 7px solid transparent;
+                border-right: 7px solid transparent;
+                border-bottom: 7px solid #fff;
+                transform: translateX(-50%);
+            }
+        }
+
+        &__action {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            gap: 8px;
+        }
+
+        &__action-main {
+            min-width: 0;
+            flex: 1;
+        }
+
+        &__icon {
+            display: flex;
+            align-items: center;
+            gap: 4px;
+
+            img {
+                width: 18px;
+                height: 16px;
+            }
+        }
+
+        &__reason {
+            color: var(--tracking-primary);
+            font-weight: 500;
+        }
+
+        &__issue {
+            margin-top: 4px;
+            color: var(--tracking-primary);
+            font-size: 12px;
+            line-height: 17px;
+        }
+
+        &__btn {
+            display: flex;
+            align-items: center;
+            flex-shrink: 0;
+            gap: 2px;
+            padding: 8px 10px;
+            border-radius: 6px;
+            background: var(--tracking-primary);
+            color: #fff;
+            font-size: 13px;
+        }
+    }
+
+    .crop-archive {
+        margin-top: 16px;
+        padding: 16px 12px;
+        border-radius: 12px;
+        background: #fff;
+
+        &__title {
+            position: relative;
+            display: inline-block;
+            z-index: 1;
+            color: #1F1F1F;
+            font-size: 16px;
+            font-weight: 600;
+            line-height: 22px;
+
+            &::after {
+                content: "";
+                position: absolute;
+                left: 0;
+                bottom: 2px;
+                z-index: -1;
+                width: 2em;
+                height: 6px;
+                border-radius: 2px;
+                background: rgba(91, 184, 255, 0.55);
+            }
+        }
+
+        &__year {
+            margin: 4px 0 12px;
+            color: rgba(0, 0, 0, 0.45);
+            font-size: 12px;
+        }
+    }
 }
 </style>

+ 157 - 0
src/views/old_mini/agri_file/pages/growthTrack.vue

@@ -0,0 +1,157 @@
+<template>
+    <div class="growth-track-page">
+        <custom-header :name="t('agriFile.workName')" />
+        <div class="growth-hero">
+            <div class="growth-hero__top">
+                <div class="growth-hero__main">
+                    <div class="growth-hero__title-row">
+                        <span class="growth-hero__title">{{ theme }}</span>
+                        <span class="growth-hero__tag growth-hero__tag--level">
+                            <el-icon class="growth-hero__bell">
+                                <Bell />
+                            </el-icon>
+                            {{ level }}
+                        </span>
+                        <span class="growth-hero__tag growth-hero__tag--zone">{{ zoneName }}</span>
+                    </div>
+                    <div class="growth-hero__reason">{{ reason }}</div>
+                </div>
+                <div class="growth-hero__share">
+                    <el-icon>
+                        <Right />
+                    </el-icon>
+                    <span>{{ t("agriFile.forward") }}</span>
+                </div>
+            </div>
+        </div>
+        <div class="growth-content">
+            <div class="growth-content__title">{{ t("agriFile.agriAssessment") }}</div>
+            <div class="growth-content__desc">{{ t("agriFile.agriAssessmentDesc") }}</div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { Bell, Right } from "@element-plus/icons-vue";
+import customHeader from "@/components/customHeader.vue";
+import { useI18n } from "@/i18n";
+
+const { t } = useI18n();
+
+const theme = computed(() => t("agriFile.interactTheme"));
+const level = computed(() => t("agriFile.riskLevel2"));
+const reason = computed(() => t("agriFile.interactReason"));
+const zoneName = computed(() => t("agriFile.zoneOne"));
+</script>
+
+<style lang="scss" scoped>
+.growth-track-page {
+    width: 100%;
+    min-height: 100vh;
+    background: #f5f5f5;
+
+    .growth-hero {
+        flex-shrink: 0;
+        padding: 14px 12px 28px;
+        background: #2199f8;
+
+        &__top {
+            display: flex;
+            align-items: flex-start;
+            justify-content: space-between;
+            gap: 10px;
+        }
+
+        &__main {
+            min-width: 0;
+            flex: 1;
+        }
+
+        &__title-row {
+            display: flex;
+            flex-wrap: wrap;
+            align-items: center;
+            gap: 6px;
+        }
+
+        &__title {
+            color: #fff;
+            font-size: 20px;
+            font-weight: 600;
+            line-height: 28px;
+        }
+
+        &__tag {
+            display: inline-flex;
+            align-items: center;
+            flex-shrink: 0;
+            gap: 2px;
+            height: 20px;
+            padding: 0 8px;
+            border-radius: 20px;
+            background: #fff;
+            font-size: 12px;
+            line-height: 20px;
+        }
+
+        &__tag--level {
+            color: #ff6a6a;
+        }
+
+        &__tag--zone {
+            color: #2199f8;
+        }
+
+        &__bell {
+            font-size: 12px;
+        }
+
+        &__reason {
+            margin-top: 10px;
+            padding: 6px 8px;
+            border-radius: 4px;
+            background: rgba(255, 255, 255, 0.16);
+            color: #fff;
+            font-size: 13px;
+            line-height: 20px;
+        }
+
+        &__share {
+            display: flex;
+            align-items: center;
+            flex-shrink: 0;
+            gap: 2px;
+            margin-top: 4px;
+            padding: 4px 10px;
+            border-radius: 16px;
+            background: rgba(13, 92, 173, 0.35);
+            color: #fff;
+            font-size: 12px;
+        }
+    }
+
+    .growth-content {
+        position: relative;
+        z-index: 1;
+        margin: -16px 10px 12px;
+        padding: 14px 12px 16px;
+        border-radius: 12px;
+        background: #fff;
+
+        &__title {
+            color: #1f1f1f;
+            font-size: 16px;
+            font-weight: 600;
+            line-height: 22px;
+        }
+
+        &__desc {
+            margin-top: 8px;
+            color: rgba(0, 0, 0, 0.45);
+            font-size: 13px;
+            line-height: 20px;
+        }
+    }
+}
+</style>