ソースを参照

feat: 农情研判

lxf 1 週間 前
コミット
45a10437bd

BIN
src/assets/img/report/type-bg.png


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


+ 2 - 0
src/i18n/messages.js

@@ -76,6 +76,7 @@ export default {
         growthReport: {
             title: "长势报告",
             cropTitle: "作物长势报告",
+            expand: "展开",
             weatherRiskReport: "气象风险报告",
             switchCategory: "切换品类",
             noRiskData: "暂无气象风险数据",
@@ -449,6 +450,7 @@ export default {
         growthReport: {
             title: "Growth Report",
             cropTitle: "Crop Growth Report",
+            expand: "Expand",
             weatherRiskReport: "Weather RiskReport",
             switchCategory: "Switch category",
             noRiskData: "No weather risk data",

+ 480 - 0
src/views/old_mini/growth_report/components/CropAlertPanel.vue

@@ -0,0 +1,480 @@
+<template>
+    <div class="crop-alert-floating-panel">
+        <floating-panel
+            class="floating-panel"
+            :class="{ 'background-panel': isBackground }"
+            v-model:height="height"
+            :anchors="anchors"
+            :content-draggable="true"
+            @height-change="handleHeightChange"
+        >
+            <div
+                class="floating-panel-content"
+                :style="{ paddingBottom: `${tabBarHeight}px` }"
+            >
+                <div class="panel-shell" ref="panelShellRef">
+                    <div class="crop-header">
+                        <div class="crop-info">
+                            <el-avatar class="crop-avatar" :size="24" :src="cropAvatar" />
+                            <span class="crop-name">{{ currentCropName }}</span>
+                            <span class="crop-badge">当前作物</span>
+                        </div>
+                        <el-tooltip
+                            v-model:visible="cropMenuVisible"
+                            placement="bottom"
+                            trigger="click"
+                            effect="light"
+                            :show-arrow="true"
+                            popper-class="crop-switch-tooltip"
+                            :teleported="true"
+                        >
+                            <template #content>
+                                <div class="crop-option-list">
+                                    <div
+                                        v-for="item in cropOptions"
+                                        :key="item"
+                                        class="crop-option-item"
+                                        :class="{ active: item === currentCropName }"
+                                        @click="handleSelectCrop(item)"
+                                    >
+                                        {{ item }}
+                                    </div>
+                                </div>
+                            </template>
+                            <div class="switch-btn" :class="{ 'is-open': cropMenuVisible }">
+                                <span>{{ t("growthReport.switchCategory") }}</span>
+                                <icon
+                                    :name="cropMenuVisible ? 'arrow-up' : 'arrow-down'"
+                                    class="switch-btn__icon"
+                                />
+                            </div>
+                        </el-tooltip>
+                    </div>
+
+                    <div class="alert-list">
+                        <div
+                            v-for="item in alertCards"
+                            :key="item.id"
+                            class="alert-card"
+                        >
+                            <div class="alert-card__header">
+                                <div class="alert-card__title-wrap">
+                                    <img class="alert-card__icon" :src="item.icon" alt="" />
+                                    <span class="alert-card__title">{{ item.title }}</span>
+                                </div>
+                                <span class="alert-card__link" @click="handleViewDetail(item)">
+                                    查看详情
+                                </span>
+                            </div>
+                            <div class="alert-card__content-wrap">
+                                <span class="alert-card__content">{{ item.content }}</span>
+                                <span
+                                    v-if="item.patrolTip"
+                                    class="alert-card__patrol"
+                                    @click="handlePatrolTip(item)"
+                                >
+                                    <el-icon class="alert-card__patrol-icon"><Link /></el-icon>
+                                    <span>{{ item.patrolTip }}</span>
+                                </span>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </floating-panel>
+
+        <div class="expand-btn-wrap" v-show="height === defaultHeight">
+            <span>{{ t("growthReport.cropTitle") }}</span>
+            <div class="expand-btn" @click="handleExpandBtnClick">
+                <span>{{ t("growthReport.expand") }}</span>
+                <el-icon><ArrowUpBold /></el-icon>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { FloatingPanel, Icon } from "vant";
+import { ArrowUpBold, Link } from "@element-plus/icons-vue";
+import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
+import { useI18n } from "@/i18n";
+
+const { t } = useI18n();
+
+const props = defineProps({
+    cropName: {
+        type: String,
+        default: "水稻",
+    },
+    cropAvatar: {
+        type: String,
+        default: require("@/assets/img/home/banner.png"),
+    },
+    cropOptions: {
+        type: Array,
+        default: () => ["水稻", "荔枝", "香蕉"],
+    },
+});
+
+const emit = defineEmits(["switchCategory", "viewDetail", "patrolTip", "heightChange"]);
+
+const weatherIcon = require("@/assets/img/report/weather.png");
+const tabBarHeight = ref(Number(localStorage.getItem("tabBarHeight")) || 50);
+
+const currentCropName = ref(props.cropName);
+const cropMenuVisible = ref(false);
+
+watch(
+    () => props.cropName,
+    (name) => {
+        if (name) currentCropName.value = name;
+    }
+);
+
+const handleSelectCrop = (name) => {
+    if (!name || name === currentCropName.value) {
+        cropMenuVisible.value = false;
+        return;
+    }
+    currentCropName.value = name;
+    cropMenuVisible.value = false;
+    emit("switchCategory", name);
+};
+
+const PANEL_HEADER_BAR = 30; // 隐藏原生 header 后,内容顶部留白
+const PANEL_BOTTOM_GAP = 16;
+
+const defaultHeight = ref(0);
+const fullHeight = Math.round(window.innerHeight);
+const anchors = ref([defaultHeight.value, 280 + tabBarHeight.value, fullHeight]);
+const height = ref(anchors.value[1]);
+
+const isBackground = ref(false);
+const panelShellRef = ref(null);
+let resizeObserver = null;
+let syncing = false;
+
+const syncMidAnchor = () => {
+    const shell = panelShellRef.value;
+    if (!shell || syncing) return;
+
+    // scrollHeight 才是内容真实高度;offsetHeight 在被裁切时会偏小
+    const contentH = shell.scrollHeight;
+    const idealMid = Math.round(
+        contentH + PANEL_HEADER_BAR + PANEL_BOTTOM_GAP + tabBarHeight.value
+    );
+    // 留一点顶部空间,避免直接顶满屏
+    const maxMid = fullHeight - 48;
+    const nextMid = Math.max(240, Math.min(idealMid, maxMid));
+
+    if (Math.abs(nextMid - anchors.value[1]) < 2) return;
+
+    syncing = true;
+    const wasAtMid = Math.abs(height.value - anchors.value[1]) < 2;
+    anchors.value = [defaultHeight.value, nextMid, fullHeight];
+    if (wasAtMid) {
+        height.value = nextMid;
+    }
+    nextTick(() => {
+        syncing = false;
+    });
+};
+
+const alertCards = [
+    {
+        id: "warning",
+        title: "具体预警",
+        icon: weatherIcon,
+        content: "详情体预警体预警体预警体预警体预警体预详情详情详情详情详情详情详情详情详情详情",
+        patrolTip: "",
+    },
+    {
+        id: "stress",
+        title: "某某胁迫",
+        icon: weatherIcon,
+        content: "详情详情详情具体预警具体预警情详情详详情详情详情详情详情详情",
+        patrolTip: "异常态势巡园要点",
+    },
+];
+
+const handleViewDetail = (item) => {
+    emit("viewDetail", item);
+};
+
+const handlePatrolTip = (item) => {
+    emit("patrolTip", item);
+};
+
+const handleHeightChange = ({ height: nextHeight }) => {
+    isBackground.value = nextHeight > anchors.value[1];
+    emit("heightChange", nextHeight);
+};
+
+const handleExpandBtnClick = () => {
+    height.value = anchors.value[1];
+    emit("heightChange", anchors.value[1]);
+};
+
+onMounted(() => {
+    nextTick(() => {
+        // 等布局完成后再量一次,避免首屏高度偏小
+        requestAnimationFrame(() => {
+            syncMidAnchor();
+            setTimeout(() => {
+                syncMidAnchor();
+                emit("heightChange", height.value);
+            }, 80);
+        });
+        if (!panelShellRef.value || typeof ResizeObserver === "undefined") return;
+        resizeObserver = new ResizeObserver(() => {
+            syncMidAnchor();
+            emit("heightChange", height.value);
+        });
+        resizeObserver.observe(panelShellRef.value);
+    });
+});
+
+onBeforeUnmount(() => {
+    resizeObserver?.disconnect?.();
+    resizeObserver = null;
+});
+</script>
+
+<style lang="scss" scoped>
+.van-floating-panel {
+    border-radius: 0;
+}
+
+.floating-panel {
+    background: transparent;
+
+    ::v-deep {
+        .van-floating-panel__content {
+            background: transparent;
+            overflow-y: auto;
+        }
+
+        // 隐藏拖拽条,并取消其占位
+        .van-floating-panel__header {
+            height: 0 !important;
+            min-height: 0 !important;
+            padding: 0 !important;
+            margin: 0 !important;
+            overflow: hidden;
+            opacity: 0;
+            pointer-events: none;
+        }
+    }
+
+    .floating-panel-content {
+        width: calc(100% - 20px);
+        margin: 30px auto 0; // 补偿隐藏 header 的 30px,整体下移
+        box-sizing: border-box;
+    }
+
+    .panel-shell {
+        padding: 12px;
+        border-radius: 16px;
+        background: linear-gradient(180deg, #b2dbfb 0%, #cbe8ff 100%);
+        box-sizing: border-box;
+    }
+
+    .crop-header {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        gap: 10px;
+        margin-bottom: 10px;
+    }
+
+    .crop-info {
+        display: flex;
+        align-items: center;
+        gap: 6px;
+        min-width: 0;
+    }
+
+    .crop-avatar {
+        flex-shrink: 0;
+    }
+
+    .crop-name {
+        font-size: 18px;
+        font-weight: 500;
+        color: #000000;
+        line-height: 26px;
+    }
+
+    .crop-badge {
+        flex-shrink: 0;
+        height: 20px;
+        padding: 0 5px;
+        border-radius: 2px;
+        background: #2199f8;
+        color: #fff;
+        backdrop-filter: blur(4px);
+        font-size: 12px;
+        line-height: 20px;
+    }
+
+    .switch-btn {
+        display: flex;
+        align-items: center;
+        gap: 2px;
+        flex-shrink: 0;
+        height: 28px;
+        padding: 0 10px;
+        border-radius: 14px;
+        border: 1px solid #2199f8;
+        background: #fff;
+        color: #2199f8;
+        font-size: 12px;
+
+        &.is-open {
+            border-color: #2199f8;
+            box-shadow: 0 0 0 1px rgba(33, 153, 248, 0.15);
+        }
+
+        &__icon {
+            font-size: 12px;
+            color: #2199f8;
+        }
+    }
+
+    .alert-list {
+        display: flex;
+        flex-direction: column;
+        gap: 10px;
+    }
+
+    .alert-card {
+        padding: 12px;
+        border-radius: 12px;
+        background: #fff;
+        box-sizing: border-box;
+
+        &__header {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            gap: 8px;
+        }
+
+        &__title-wrap {
+            display: flex;
+            align-items: flex-end;
+            gap: 6px;
+            min-width: 0;
+        }
+
+        &__icon {
+            width: 17px;
+            height: 17px;
+            flex-shrink: 0;
+        }
+
+        &__title {
+            font-size: 14px;
+            font-weight: 500;
+            color: #000000;
+        }
+
+        &__link {
+            flex-shrink: 0;
+            font-size: 12px;
+            color: #2199f8;
+        }
+
+        &__content-wrap {
+            margin-top: 8px;
+        }
+
+        &__content {
+            font-size: 14px;
+            line-height: 21px;
+            color: rgba(6, 6, 6, 0.5);
+            word-break: break-all;
+        }
+
+        &__patrol {
+            padding-left: 6px;
+            display: inline-flex;
+            align-items: center;
+            gap: 4px;
+            font-size: 14px;
+            color: #2199f8;
+            text-decoration: underline;
+            text-underline-offset: 4px;
+        }
+
+        &__patrol-icon {
+            font-size: 16px;
+        }
+    }
+
+    &.background-panel {
+        background: #f5f7fb;
+    }
+}
+
+.expand-btn-wrap {
+    position: absolute;
+    bottom: 62px;
+    left: 12px;
+    width: calc(100% - 24px);
+    background-image: linear-gradient(180deg, #d7eafc 0%, #ffffff 100%);
+    border-radius: 14px;
+    padding: 15px 12px;
+    box-sizing: border-box;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    font-weight: 500;
+    font-size: 15px;
+
+    .expand-btn {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: 4px;
+        font-size: 13px;
+        color: #2199f8;
+    }
+}
+</style>
+
+<style lang="scss">
+.crop-switch-tooltip {
+    z-index: 3000 !important;
+    padding: 8px 0 !important;
+    min-width: 120px;
+    border: none !important;
+    border-radius: 12px !important;
+    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12) !important;
+    background: #fff !important;
+
+    .el-popper__arrow::before {
+        background: #fff !important;
+        border: none !important;
+    }
+
+    .crop-option-list {
+        display: flex;
+        flex-direction: column;
+    }
+
+    .crop-option-item {
+        padding: 10px 20px;
+        font-size: 14px;
+        line-height: 20px;
+        color: #333;
+        cursor: pointer;
+
+        &:active,
+        &.active {
+            color: #2199f8;
+            background: rgba(33, 153, 248, 0.06);
+        }
+    }
+}
+</style>

+ 2 - 2
src/views/old_mini/growth_report/growthReportMap.js

@@ -28,8 +28,8 @@ class IndexMap {
       style: (f) => {
         return new Style({
           image: new Icon({
-            src: require("@/assets/img/map/map_point.png"),
-            scale: 0.5,
+            src: require("@/assets/img/home/garden-point.png"),
+            scale: 0.4,
           }),
         });
       },

+ 178 - 355
src/views/old_mini/growth_report/index.vue

@@ -1,284 +1,146 @@
 <template>
     <div class="growth-report-page" :style="{ height: `calc(100vh - ${tabBarHeight}px)` }">
-        <!-- 天气遮罩 -->
-        <div class="weather-mask" v-show="isExpanded" @click="handleMaskClick"></div>
-        <!-- 头部 -->
-        <div
-            class="growth-report-header"
-            :style="activeGardenTab === 'current' ? headerMotionStyle : undefined"
-        >
-            <weather-info ref="weatherInfoRef" :hasWeather="false" from="growth_report" class="weather-info"
-                @weatherExpanded="weatherExpanded" @changeGarden="changeGarden" @changeGardenTab="changeGardenTab"
-                @reportTabClick="handleReportTabClick" :isGarden="true" :gardenId="defaultGardenId" />
+        <div class="report-content">
+            <div class="map-container" ref="mapContainer"></div>
         </div>
-        <!-- 农场列表 -->
-        <div v-show="activeGardenTab === 'list'">
-            <garden-list ref="gardenListRef" :garden-id="selectedGardenId" @loaded="handleGardenLoaded"
-                @selectGarden="handleGardenSelected" />
+
+        <!-- 左上角:位置切换 -->
+        <div class="location-bar" @click="handleSwitchLocation">
+            <el-icon class="location-bar__icon"><LocationFilled /></el-icon>
+            <span class="location-bar__name van-ellipsis">{{ locationName }}</span>
+            <span class="location-bar__action">切换位置</span>
         </div>
-        <div class="report-content" v-show="activeGardenTab === 'current'">
-            <div class="map-legend" :class="{ 'map-legend--en': locale === 'en' }">
-                <div
-                    v-for="item in mapLegendItems"
-                    :key="item.key"
-                    class="map-legend__item"
-                >
-                    <span class="map-legend__pill" :class="item.pillClass"></span>
-                    <span class="map-legend__text">{{ item.label }}</span>
+
+        <!-- 左侧:胁迫图层菜单 -->
+        <div class="side-menu side-menu--left">
+            <div
+                v-for="item in stressMenuItems"
+                :key="item.key"
+                class="side-menu__item"
+                :class="{ active: activeStressKey === item.key }"
+                @click="activeStressKey = item.key"
+            >
+                <img class="side-menu__icon" :src="item.icon" alt="" />
+                <div class="side-menu__text">
+                    <span>{{ item.line1 }}</span>
+                    <span>{{ item.line2 }}</span>
                 </div>
             </div>
-            <div class="map-container" ref="mapContainer"></div>
         </div>
-        <div class="invite-group">
-            <div class="invite-btn" @click="handleInvite('farmer')">
-                <img class="invite-icon" src="@/assets/img/home/user-icon.png" alt="">
-                邀请农服</div>
-            <div class="invite-btn">
-                <img class="invite-icon" src="@/assets/img/home/user-icon.png" alt="">
-                邀请农户</div>
+
+        <!-- 右侧:图层菜单 -->
+        <div class="side-menu side-menu--right">
+            <div
+                v-for="item in layerMenuItems"
+                :key="item.key"
+                class="side-menu__item"
+                :class="{ active: activeLayerKey === item.key }"
+                @click="activeLayerKey = item.key"
+            >
+                <img class="side-menu__icon" :src="item.icon" alt="" />
+                <div class="side-menu__text">
+                    <span>{{ item.line1 }}</span>
+                    <span>{{ item.line2 }}</span>
+                </div>
+            </div>
         </div>
 
-        <risk-report-panel
-            v-show="activeGardenTab === 'current'"
-            :view-type="panelViewType"
-            :plot-detail="plotDetail"
-            @expand-progress="panelExpandProgress = $event"
-            @close-plot-detail="handleClosePlotDetail"
+        <crop-alert-panel
+            :crop-name="currentCropName"
+            @switch-category="handleSwitchCategory"
+            @view-detail="handleViewDetail"
+            @patrol-tip="handlePatrolTip"
         />
     </div>
 </template>
 
 <script setup>
-import { computed, nextTick, onActivated, ref } from "vue";
-import { useRoute, useRouter } from "vue-router";
+import { computed, nextTick, onActivated, onMounted, ref } from "vue";
 import { useStore } from "vuex";
-import { useI18n } from "@/i18n";
-import weatherInfo from "@/components/weatherInfo.vue";
-import gardenList from "@/components/gardenList.vue";
+import { ElMessage } from "element-plus";
+import { LocationFilled } from "@element-plus/icons-vue";
+import { convertPointToArray } from "@/utils/index";
 import GrowthReportMap from "./growthReportMap.js";
-import RiskReportPanel from "./components/RiskReportPanel.vue";
-import * as util from "@/common/ol_common.js";
-import wx from "weixin-js-sdk";
-
-const DEFAULT_FARM_POINT = "POINT(113.6142086995688 23.585836479509055)";
-
-function isPointWkt(value) {
-    return typeof value === "string" && /^POINT\s*\(/i.test(value.trim());
-}
-
-function isPolygonWkt(value) {
-    return typeof value === "string" && /^(MULTI)?POLYGON\s*\(/i.test(value.trim());
-}
-
-function resolveFarmPolygonWkt(farm) {
-    if (!farm || typeof farm === "string") return null;
-    const polygon = farm.farm_polygon;
-    return isPolygonWkt(polygon) ? polygon : null;
-}
+import CropAlertPanel from "./components/CropAlertPanel.vue";
 
-function resolveFarmLocationWkt(farm, polygonWkt) {
-    if (typeof farm === "string" && isPointWkt(farm)) return farm;
-    if (!farm || typeof farm === "string") return null;
+const DEFAULT_MAP_POINT = "POINT(113.6142086995688 23.585836479509055)";
+const MAP_KEY = "CZLBZ-LJICQ-R4A5J-BN62X-YXCRJ-GNBUT";
 
-    const candidates = [farm.wkt, farm.geom_wkt, farm.farm_location];
-    for (const candidate of candidates) {
-        if (isPointWkt(candidate)) return candidate;
-    }
-
-    if (polygonWkt) {
-        const geom = util.wktCastGeom(polygonWkt);
-        const extent = geom.getExtent();
-        return `POINT(${(extent[0] + extent[2]) / 2} ${(extent[1] + extent[3]) / 2})`;
-    }
-
-    return null;
-}
+const menuIcon = require("@/assets/img/common/farm-active.png");
 
 const store = useStore();
-const { t, locale } = useI18n();
-const route = useRoute();
-const router = useRouter();
 const tabBarHeight = computed(() => store.state.home.tabBarHeight);
 
-const isExpanded = ref(false);
-const weatherInfoRef = ref(null);
-const defaultGardenId = ref(null);
-const selectedGardenId = ref(null);
-const gardenListRef = ref(null);
-const activeGardenTab = ref("current");
-const panelExpandProgress = ref(0);
-const panelViewType = ref("risk");
-const plotDetail = ref(createDefaultPlotDetail());
-
-const HEADER_FADE_START = 0.68;
-
-function createDefaultPlotDetail() {
-    const defaultImage = require("@/assets/img/home/banner.png");
-    return {
-        name: "地块名称",
-        area: "289亩",
-        categories: "荔枝、水稻",
-        startTime: "2026/06/05",
-        harvestTime: "2026/06/05",
-        description: "当前处于物候期,有什么风险,做了什么农事,或者正在执行中,当前处于物候期,有什么风险",
-        images: [defaultImage, defaultImage, defaultImage, defaultImage],
-    };
-}
-
-function buildPlotDetailByTab(item) {
-    const tabLabelMap = {
-        soilImprovement: "土壤改良地块",
-        rotationAdvice: "轮作建议地块",
-    };
-    return {
-        ...createDefaultPlotDetail(),
-        name: tabLabelMap[item.key] || "地块名称",
-    };
-}
-
-const handleClosePlotDetail = () => {
-    panelViewType.value = "risk";
-};
-
-const headerMotionStyle = computed(() => {
-    const progress = panelExpandProgress.value;
-    const fade = progress <= HEADER_FADE_START
-        ? 0
-        : (progress - HEADER_FADE_START) / (1 - HEADER_FADE_START);
-    const opacity = 1 - fade;
-    return {
-        opacity,
-        transform: `translateY(${-14 * fade}px) scale(${1 - fade * 0.04})`,
-        pointerEvents: opacity < 0.15 ? "none" : "auto",
-    };
-});
-
-const currentFarmName = ref("");
-const currentFarmVariety = ref(null);
 const mapContainer = ref(null);
 const growthReportMap = new GrowthReportMap();
+const locationName = ref("");
+const currentCropName = ref("水稻");
+const activeStressKey = ref("hot-drought-2");
+const activeLayerKey = ref("growth");
+
+const stressMenuItems = [
+    { key: "stress", line1: "胁迫", line2: "胁迫", icon: menuIcon },
+    { key: "hot-drought-1", line1: "高温", line2: "干旱", icon: menuIcon },
+    { key: "hot-drought-2", line1: "高温", line2: "干旱", icon: menuIcon },
+    { key: "hot-drought-3", line1: "高温", line2: "干旱", icon: menuIcon },
+];
+
+const layerMenuItems = [
+    { key: "growth", line1: "作物", line2: "长势", icon: menuIcon },
+    { key: "phenology", line1: "物候", line2: "进程", icon: menuIcon },
+];
+
+function getLocationName() {
+    const locationPoint = localStorage.getItem("selectedFarmPoint") || store.state.home.miniUserLocationPoint;
+    if (!locationPoint) return;
+    const farmLocation = convertPointToArray(locationPoint);
+    if (!farmLocation?.length) return;
+    const params = {
+        key: MAP_KEY,
+        location: `${farmLocation[1]},${farmLocation[0]}`,
+    };
+    VE_API.old_mini_map.location(params).then(({ result }) => {
+        locationName.value = result?.address_component
+            ? result.address_component.city + result.address_component.district
+            : result?.address + "";
+    });
+}
 
-const syncMapByFarm = async (farm) => {
-    const polygonWkt = resolveFarmPolygonWkt(farm);
-    const location = resolveFarmLocationWkt(farm, polygonWkt) || DEFAULT_FARM_POINT;
-
+const initMap = async () => {
     await nextTick();
     if (!mapContainer.value) return;
-
     if (growthReportMap.kmap) {
         growthReportMap.kmap.map?.updateSize?.();
-        if (polygonWkt) {
-            growthReportMap.setAreaGeometry([polygonWkt]);
-            const coordinate = util.wktCastGeom(location).getFirstCoordinate();
-            growthReportMap.setMapPoint(coordinate);
-            growthReportMap.scheduleFitView();
-        } else {
-            growthReportMap.clearLayer();
-            const coordinate = util.wktCastGeom(location).getFirstCoordinate();
-            growthReportMap.setMapPosition(coordinate);
-        }
         return;
     }
-
+    const location = store.state.home.miniUserLocationPoint || DEFAULT_MAP_POINT;
     growthReportMap.initMap(location, mapContainer.value);
-    if (polygonWkt) {
-        growthReportMap.setAreaGeometry([polygonWkt]);
-        growthReportMap.scheduleFitView();
-    }
 };
 
-const initGrowthReportMap = async () => {
-    await syncMapByFarm();
+const handleSwitchLocation = () => {
+    ElMessage.info("切换位置功能即将开放");
 };
 
-const mapLegendItems = computed(() => [
-    { key: "zone", label: t("agriFile.legendZone"), pillClass: "map-legend__pill--zone" },
-    { key: "growth", label: t("agriFile.legendGrowth"), pillClass: "map-legend__pill--growth" },
-    { key: "pest", label: t("agriFile.legendPest"), pillClass: "map-legend__pill--pest" },
-]);
-
-const handleReportTabClick = (item) => {
-    if (item.key === "historyRisk") {
-        router.push(
-            `/history_risk_report?farmVariety=${currentFarmVariety.value ?? ""}&currentFarmName=${currentFarmName.value ?? ""}`
-        );
-        return;
-    }
-    plotDetail.value = buildPlotDetailByTab(item);
-    panelViewType.value = "plot";
+const handleSwitchCategory = (cropName) => {
+    if (!cropName) return;
+    currentCropName.value = cropName;
 };
 
-const weatherExpanded = (isExpandedValue) => {
-    isExpanded.value = isExpandedValue;
+const handleViewDetail = () => {
+    ElMessage.info("详情功能即将开放");
 };
 
-const handleMaskClick = () => {
-    if (weatherInfoRef.value?.toggleExpand) {
-        weatherInfoRef.value.toggleExpand();
-    }
+const handlePatrolTip = () => {
+    ElMessage.info("巡园要点功能即将开放");
 };
 
-const changeGardenTab = (tab) => {
-    activeGardenTab.value = tab;
-    if (tab !== "current") {
-        panelExpandProgress.value = 0;
-        panelViewType.value = "risk";
-        return;
-    }
-    nextTick(() => {
-        growthReportMap.kmap?.map?.updateSize?.();
-        growthReportMap.scheduleFitView?.();
-    });
-};
-
-const handleGardenLoaded = ({ hasFarm }) => {
-    weatherInfoRef.value?.setGardenLoaded?.(hasFarm);
-};
-
-const handleGardenSelected = (garden) => {
-    selectedGardenId.value = garden?.id ?? null;
-    syncMapByFarm(garden);
-    weatherInfoRef.value?.setSelectedGarden?.(garden);
-};
-
-const changeGarden = (data) => {
-    if (!data?.id) return;
-    store.commit("home/SET_GARDEN_ID", data.id);
-    selectedGardenId.value = data.id;
-    currentFarmName.value = data.name ?? "";
-    currentFarmVariety.value = data.farm_variety ?? null;
-    syncMapByFarm(data);
-};
-
-
-const handleInvite = (item) => {
-    let inviteName = "好友";
-    try {
-        const userInfo = JSON.parse(localStorage.getItem("localUserInfo") || "{}");
-        inviteName = userInfo.nickName || userInfo.userName || userInfo.name || inviteName;
-    } catch {
-        // ignore
-    }
-    const query = {
-        askInfo: { title: "邀请完善信息", content: "是否分享该邀请给好友" },
-        shareText: "邀请您完善地块种植信息",
-        targetUrl: `entry_information`,
-        paramsPage: JSON.stringify({ inviteName }),
-        imageUrl: 'https://birdseye-img.sysuimars.com/temp/field.png',
-    };
-    wx.miniProgram.navigateTo({
-        url: `/pages/subPages/share_page/index?pageParams=${JSON.stringify(query)}&type=sharePage`,
-    });
-}
-
-onActivated(async () => {
-    if (route.query?.farmId) {
-        defaultGardenId.value = route.query.farmId;
-    }
-    const savedFarmId = localStorage.getItem("selectedFarmId");
-    selectedGardenId.value = savedFarmId ? Number(savedFarmId) : null;
-    gardenListRef.value?.refreshFarmList?.();
-    await initGrowthReportMap();
+onMounted(() => {
+    getLocationName();
+    initMap();
+});
+onActivated(() => {
+    getLocationName();
+    initMap();
 });
 </script>
 
@@ -286,144 +148,105 @@ onActivated(async () => {
 .growth-report-page {
     width: 100%;
     height: 100%;
-    background: #F5F7FB;
+    background: #f5f7fb;
     box-sizing: border-box;
 
-    .weather-mask {
+    .location-bar {
         position: fixed;
-        top: 0;
-        left: 0;
-        width: 100%;
-        height: 100%;
-        background-color: rgba(0, 0, 0, 0.52);
-        z-index: 11;
-    }
-
-    .growth-report-header {
-        position: absolute;
-        z-index: 12;
-        left: 10px;
         top: 12px;
-        width: calc(100% - 20px);
-        will-change: transform, opacity;
-        transform-origin: center top;
+        left: 12px;
+        z-index: 16;
+        display: inline-flex;
+        align-items: center;
+        gap: 4px;
+        max-width: calc(100% - 24px);
+        height: 32px;
+        padding: 0 12px;
+        border-radius: 16px;
+        background: rgba(0, 0, 0, 0.4);
+        backdrop-filter: blur(4px);
+        color: #fff;
+        font-size: 14px;
+        box-sizing: border-box;
 
-        .weather-info {
-            width: 100%;
-            position: relative;
-            left: auto;
-            top: auto;
-
-            :deep(.garden-tabs) {
-                .garden-item.left-item.active .current-name {
-                    color: #2199F8;
-                    font-weight: 600;
-                }
-            }
+        &__icon {
+            font-size: 16px;
+            flex-shrink: 0;
         }
-    }
-    .invite-group {
-        position: fixed;
-        bottom: 300px;
-        right: 16px;
-        z-index: 10;
-        pointer-events: none;
-        .invite-btn {
-            pointer-events: auto;
-            height: 32px;
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            border-radius: 5px;
-            background: #fff;
-            box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.1);
-            gap: 4px;
-            padding: 0 8px;
-            color: #2199F8;
-            font-weight: 500;
-            font-size: 12px;
-            .invite-icon {
-                width: 16px;
-            }
+
+        &__name {
+            max-width: calc(100vw - 80px);
         }
-        .invite-btn + .invite-btn {
-            margin-top: 10px;
+
+        &__action {
+            flex-shrink: 0;
+            color: #2199f8;
         }
     }
 
-    .report-content {
-        position: relative;
-        height: 100%;
+    .side-menu {
+        position: fixed;
+        top: 58px;
+        z-index: 15;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        gap: 12px;
+        padding: 10px 6px;
+        border-radius: 6px;
+        background: #fff;
+        box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);
         box-sizing: border-box;
 
-        .map-legend {
-            position: absolute;
-            top: 110px;
+        &--left {
+            left: 10px;
+        }
+
+        &--right {
             right: 10px;
-            z-index: 15;
+        }
+
+        &__item {
             display: flex;
+            flex-direction: column;
             align-items: center;
-            gap: 10px;
-            padding: 4px 10px;
-            background: rgba(0, 0, 0, 0.46);
-            backdrop-filter: blur(4px);
-            border-radius: 4px;
-            box-sizing: border-box;
-
-            &--en {
-                flex-direction: column;
-                align-items: flex-start;
-                gap: 6px;
-                padding: 8px 10px;
-                background: rgba(0, 0, 0, 0.72);
-                border-radius: 6px;
-
-                .map-legend__item {
-                    gap: 6px;
-                }
+            gap: 4px;
+            width: 26px;
+            color: #8a8a8a;
+            cursor: pointer;
 
-                .map-legend__pill {
-                    width: 18px;
-                    height: 4px;
-                    flex-shrink: 0;
-                }
+            &.active {
+                color: #2199f8;
 
-                .map-legend__text {
-                    font-size: 11px;
-                    line-height: 1.2;
-                    white-space: nowrap;
+                .side-menu__icon {
+                    opacity: 1;
+                    filter: brightness(0) saturate(100%) invert(48%) sepia(98%) saturate(1805%) hue-rotate(178deg) brightness(98%) contrast(97%);
                 }
             }
+        }
 
-            &__item {
-                display: flex;
-                align-items: center;
-                gap: 5px;
-            }
-
-            &__pill {
-                width: 16px;
-                height: 5px;
-                border-radius: 10px;
-
-                &--zone {
-                    background: #13a27f;
-                }
-
-                &--growth {
-                    background: #ff9138;
-                }
-
-                &--pest {
-                    background: #e62e2d;
-                }
-            }
+        &__icon {
+            width: 16px;
+            height: 16px;
+            object-fit: contain;
+            opacity: 0.55;
+            filter: grayscale(1);
+        }
 
-            &__text {
-                font-size: 12px;
-                color: #fff;
-            }
+        &__text {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            font-size: 12px;
+            line-height: 14px;
+            text-align: center;
         }
+    }
+
+    .report-content {
+        position: relative;
+        height: 100%;
+        box-sizing: border-box;
 
         .map-container {
             width: 100%;

+ 87 - 29
src/views/old_mini/work_detail/index.vue

@@ -9,7 +9,6 @@
                 <div class="status-l">
                     <div class="status-title">
                         <span>{{ farmData.work_name }}</span>
-                        <div class="status-tag" :style="statusTagStyle[farmData?.farm_work_type]">{{ farmWorkTypeObj[farmData?.farm_work_type] }}</div>
                     </div>
                     <div class="status-hint-card" v-if="farmData?.work_status === 0">
                         <div class="status-hint-text">{{ farmData?.best_time }}</div>
@@ -20,8 +19,11 @@
                             <span class="status-hint-question-text">{{ farmData.interaction_issue }}</span>
                         </div>
                     </div>
-                    <div class="status-sub" :style="getFarmWorkTypeColorStyle(farmData)" v-else>
-                        {{ farmData.best_time }}
+                    <div class="status-sub-wrap" :style="getFarmWorkTypeColorStyle(farmData)" v-else>
+                        <div class="status-sub" :style="statusTagStyle[farmData?.farm_work_type]">{{ farmWorkTypeObj[farmData?.farm_work_type] }}</div>
+                        <div class="status-sub">
+                            {{ farmData.best_time }}
+                        </div>
                     </div>
                 </div>
             </div>
@@ -30,14 +32,18 @@
                 <!-- 农事组信息 -->
                 <div class="group-info group-box" v-if="farmData.interaction_reason">
                     <div class="group-name">
-                        农情研判:{{ farmData.interaction_reason }}
+                        <div class="group-title">农情研判:</div>
+                        {{ farmData.interaction_reason }}
                     </div>
                 </div>
 
                 <div class="box-wrap stage-card">
                     <div class="work-info">
-                        <div class="map-box" @click="handleViewArea">
-                            <div class="map-title">{{ $t('workDetail.executionArea') }}</div>
+                        <div class="execution-area-card" @click="handleViewArea">
+                            <div class="execution-area-card__header">
+                                <span class="title">{{ $t('agriRecordDetail.executionArea') }}</span>
+                                <span class="code">ws0gefwg9tdn</span>
+                            </div>
                             <div class="map-container" ref="mapContainer"></div>
                         </div>
                         <div class="area-list">
@@ -70,6 +76,17 @@
                 </div>
 
                 <div class="box-wrap stage-card">
+                    <div class="method-switch">
+                        <div
+                            v-for="tab in methodTabs"
+                            :key="tab.value"
+                            class="method-switch__item"
+                            :class="{ active: executeMethod === tab.value }"
+                            @click="executeMethod = tab.value"
+                        >
+                            {{ tab.label }}
+                        </div>
+                    </div>
                     <div class="work-info">
                         <div class="info-item">
                             <div class="info-title"><span class="title-block"></span>{{ $t('workDetail.reason') }}</div>
@@ -308,6 +325,13 @@ watch(locale, () => {
     getDetail();
 });
 
+// 执行方式切换:机械 / 人工
+const methodTabs = [
+    { label: "机械方式", value: "machine" },
+    { label: "人工方式", value: "manual" },
+];
+const executeMethod = ref("machine");
+
 // 执行方式 Tab 配置
 const executionTabs = [
     { label: "植保机", value: 1 },
@@ -489,7 +513,7 @@ const areaMap = new AreaMap();
 
 .content-status {
     position: relative;
-    padding: 16px 12px 0 12px;
+    padding: 12px 12px 0 12px;
     color: #fff;
     z-index: 1;
     height: 92px;
@@ -523,8 +547,14 @@ const areaMap = new AreaMap();
             }
         }
 
-        .status-sub {
+        .status-sub-wrap {
             margin-top: 10px;
+            display: flex;
+            align-items: center;
+            gap: 10px;
+        }
+
+        .status-sub {
             font-size: 13px;
             padding: 2px 8px;
             background: #fff;
@@ -636,7 +666,13 @@ const areaMap = new AreaMap();
 
         .group-name {
             font-size: 14px;
-            color: #767676;
+            color: #9C9C9C;
+            .group-title {
+                font-family: "PangMenZhengDao";
+                font-size: 16px;
+                color: #4F4F4F;
+                padding-bottom: 4px;
+            }
 
             .group-name-text {
                 color: #000;
@@ -662,7 +698,7 @@ const areaMap = new AreaMap();
 }
 
 .stage-card+.stage-card {
-    margin-top: 10px;
+    margin-top: 12px;
 }
 
 .info-item+.info-item {
@@ -698,30 +734,28 @@ const areaMap = new AreaMap();
     }
 }
 
-.map-box {
-    position: relative;
+.execution-area-card {
+    .execution-area-card__header {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
 
-    .map-title {
-        z-index: 12;
-        position: absolute;
-        border-radius: 5px 0 0 5px;
-        top: 0;
-        left: 0;
-        height: 25px;
-        padding: 0 18px;
-        line-height: 25px;
-        font-size: 12px;
-        font-weight: 500;
-        color: #fff;
-        background: rgba(0, 0, 0, 0.45);
-        backdrop-filter: blur(4px);
+        .title {
+            font-size: 16px;
+        }
+
+        .code {
+            color: #565656;
+        }
     }
 
     .map-container {
-        /* 略增高,配合 fit 后更易看清整块地 */
-        height: 200px;
         width: 100%;
-        clip-path: inset(0px round 5px);
+        height: 166px;
+        margin: 12px 0 0;
+        border-radius: 8px;
+        overflow: hidden;
+        clip-path: inset(0 round 8px);
     }
 }
 
@@ -803,6 +837,30 @@ const areaMap = new AreaMap();
 }
 
 .stage-card {
+    .method-switch {
+        display: flex;
+        gap: 10px;
+        margin-bottom: 10px;
+
+        .method-switch__item {
+            height: 28px;
+            line-height: 28px;
+            padding: 0 12px;
+            text-align: center;
+            font-size: 12px;
+            color: #8E8E8E;
+            background: #fff;
+            border: 1px solid #fff;
+            border-radius: 2px;
+            box-sizing: border-box;
+
+            &.active {
+                color: #2199F8;
+                background: rgba(33, 153, 248, 0.1);
+                border-color: #2199f8;
+            }
+        }
+    }
 
     .stage-header {
         padding-bottom: 12px;

+ 11 - 41
src/views/old_mini/work_execute/index.js

@@ -6,9 +6,9 @@ import Photo from "ol-ext/style/Photo";
 import { Icon, Stroke } from "ol/style.js";
 import { newPoint } from "@/utils/map";
 
-const LABEL_FONT = "bold 12px sans-serif";
+const LABEL_FONT = "12px sans-serif";
 const LABEL_PADDING = [4, 10, 4, 10];
-const LABEL_RADIUS = 8;
+const LABEL_RADIUS = 18;
 const LABEL_DPR = window.devicePixelRatio || 2;
 const PHOTO_RESIZE = "?imageView2/1/w/300/interlace/1";
 const DEFAULT_PHOTO =
@@ -47,10 +47,10 @@ function createPillLabel(text) {
   ctx.font = LABEL_FONT;
   ctx.beginPath();
   ctx.roundRect(0, 0, width, height, Math.min(LABEL_RADIUS, height / 2));
-  ctx.fillStyle = "#2199F8";
+  ctx.fillStyle = "#FFFFFF";
   ctx.fill();
 
-  ctx.fillStyle = "#fff";
+  ctx.fillStyle = "#0A0A0A";
   ctx.textAlign = "center";
   ctx.textBaseline = "middle";
   ctx.fillText(labelText, Math.round(width / 2), Math.round(height / 2));
@@ -66,23 +66,16 @@ class IndexMap {
     let that = this;
     let vectorStyle = new KMap.VectorStyle();
     this.vectorStyle = vectorStyle;
-    this.photoStyleCache = {};
     this.labelStyleCache = {};
 
     this.borderStyle = new Style({
-      image: new Photo({
-        src: require("@/assets/img/map/garden-border.png"),
-        radius: 24,
-        shadow: 0,
-        crop: false,
+      image: new Icon({
+        src: require("@/assets/img/home/garden-point.png"),
+        scale: 0.3,
         onload: function () {
           that.gardenPointLayer.layer.changed();
         },
-        displacement: [0, -6],
-        stroke: new Stroke({
-          width: 0,
-          color: "#fdfcfc00",
-        }),
+        danchor: [0.5,1],
       }),
     });
 
@@ -90,40 +83,19 @@ class IndexMap {
       minZoom: 6,
       maxZoom: 22,
       style: (feature) => {
-        const photoSrc = getFeaturePhotoSrc(feature);
-        if (!this.photoStyleCache[photoSrc]) {
-          this.photoStyleCache[photoSrc] = new Style({
-            image: new Photo({
-              src: photoSrc,
-              crossOrigin: "anonymous",
-              radius: 19,
-              shadow: 0,
-              crop: true,
-              onload: function () {
-                that.gardenPointLayer.layer.changed();
-              },
-              displacement: [-1, -1],
-              stroke: new Stroke({
-                width: 2,
-                color: "#fdfcfc00",
-              }),
-            }),
-          });
-        }
-
-        const labelText = feature.get("farmName") || feature.get("mapInfo") || "";
+        const labelText = (feature.get("executeDate") + ' ' + feature.get("farmName")) || feature.get("mapInfo") || "";
         if (!this.labelStyleCache[labelText]) {
           this.labelStyleCache[labelText] = new Style({
             image: new Icon({
               src: createPillLabel(labelText),
               scale: 1 / LABEL_DPR,
               anchor: [0.5, 1],
-              displacement: [0, 32],
+              displacement: [0, 28],
             }),
           });
         }
 
-        return [this.photoStyleCache[photoSrc], this.borderStyle, this.labelStyleCache[labelText]];
+        return [this.borderStyle, this.labelStyleCache[labelText]];
       },
     });
   }
@@ -138,8 +110,6 @@ class IndexMap {
   }
 
   initData(taskList, label, pointType = "point") {
-    this.gardenPointLayer.source.clear();
-    this.photoStyleCache = {};
     this.labelStyleCache = {};
     if (taskList.length > 0) {
       for (let item of taskList) {

+ 18 - 4
src/views/old_mini/work_execute/index.vue

@@ -80,7 +80,9 @@ const taskList = ref([
         cornerTip: "发现异常后机动执行",
         analysisSummary: "近期高温高湿,蒂蛀虫风险上升",
         areaCode: "A-01 / 分区一",
-        wkt: ""
+        farmName: "喷施杀菌剂",
+        executeDate: "08/06",
+        wkt: "POINT(113.612409 23.587036)",
     },
     {
         id: 2,
@@ -89,6 +91,9 @@ const taskList = ref([
         cornerTip: "",
         analysisSummary: "转色期养分需求增加,需补充钾肥",
         areaCode: "B-03 / 分区二",
+        farmName: "叶面追肥",
+        executeDate: "08/06",
+        wkt: "POINT(113.615809 23.586636)",
     },
     {
         id: 3,
@@ -97,6 +102,9 @@ const taskList = ref([
         cornerTip: "发现异常后机动执行",
         analysisSummary: "土壤墒情偏低,局部出现轻度干旱胁迫",
         areaCode: "C-02 / 分区三",
+        farmName: "灌溉补水",
+        executeDate: "08/06",
+        wkt: "POINT(113.613609 23.584436)",
     },
     {
         id: 4,
@@ -105,6 +113,9 @@ const taskList = ref([
         cornerTip: "",
         analysisSummary: "重点关注新梢与果实表面异常斑点",
         areaCode: "D-05 / 全园",
+        executeDate: "08/06",
+        farmName: "病虫害巡查",
+        wkt: "",
     },
 ]);
 
@@ -128,12 +139,14 @@ const handleViewDetail = (item) => {
 
 const mapPoint = ref(null);
 
+const getMapMarkerList = () => taskList.value.filter((item) => item.wkt);
+
 onMounted(() => {
-    mapPoint.value = store.state.home.miniUserLocationPoint;
+    mapPoint.value = store.state.home.miniUserLocationPoint || "POINT(113.614209 23.585836)";
     nextTick(() => {
         if (mapContainer.value) {
             indexMap.initMap(mapPoint.value, mapContainer.value, true);
-            indexMap.initData([], "", "farmPoint");
+            indexMap.initData(getMapMarkerList(), "farmName", "wkt");
         }
     });
 });
@@ -142,8 +155,9 @@ onActivated(() => {
     nextTick(() => {
         if (!indexMap.kmap) {
             if (mapContainer.value) {
-                mapPoint.value = store.state.home.miniUserLocationPoint;
+                mapPoint.value = store.state.home.miniUserLocationPoint || "POINT(113.614209 23.585836)";
                 indexMap.initMap(mapPoint.value, mapContainer.value, true);
+                indexMap.initData(getMapMarkerList(), "farmName", "wkt");
             }
             return;
         }