Browse Source

feat:开发诊断报告

wangsisi 4 hours ago
parent
commit
be402c2ae7

BIN
src/assets/img/report/qust-icon.png


+ 6 - 0
src/router/globalRoutes.js

@@ -199,4 +199,10 @@ export default [
         meta: { showTabbar: true, keepAlive: true },
         meta: { showTabbar: true, keepAlive: true },
         component: () => import("@/views/old_mini/agri_file/pages/diagnosisReport.vue"),
         component: () => import("@/views/old_mini/agri_file/pages/diagnosisReport.vue"),
     },
     },
+    // 土壤感知
+    {
+        path: "/soil_sense",
+        name: "SoilSense",
+        component: () => import("@/views/old_mini/agri_file/pages/soilSense.vue"),
+    },
 ];
 ];

+ 193 - 0
src/views/old_mini/agri_file/components/editPlantPointPopup.vue

@@ -0,0 +1,193 @@
+<template>
+    <Popup
+        class="edit-plant-point-popup"
+        teleport="body"
+        :z-index="9999"
+        round
+        closeable
+        v-model:show="visible"
+        @opened="handleOpened"
+        @closed="handleClosed"
+    >
+        <div class="popup-title">编辑种植点位</div>
+
+        <div class="map-preview" ref="mapRef" @click="handleMapClick"></div>
+
+        <div class="popup-section">
+            <div class="popup-section__title">请调整坡度信息</div>
+            <div class="slope-row">
+                <div
+                    v-for="item in slopeList"
+                    :key="item.key"
+                    class="slope-tag"
+                    :class="{ selected: slopeKey === item.key }"
+                    @click="slopeKey = item.key"
+                >
+                    {{ item.label }}
+                </div>
+            </div>
+        </div>
+
+        <div class="popup-confirm" @click="handleConfirm">确认信息</div>
+    </Popup>
+</template>
+
+<script setup>
+import { nextTick, ref } from "vue";
+import { useStore } from "vuex";
+import { Popup, showToast } from "vant";
+import SelectLocationMap, { POINT_ICON } from "@/views/old_mini/entry_information/map/selectLocationMap.js";
+
+const emit = defineEmits(["confirm"]);
+const store = useStore();
+
+const DEFAULT_POINT = "POINT(113.6142086995688 23.585836479509055)";
+
+const visible = ref(false);
+const mapRef = ref(null);
+const pointWkt = ref("");
+let previewMap = null;
+
+/** 坡度选项 */
+const slopeList = [
+    { key: "lt5", label: "≤ 5°" },
+    { key: "5to15", label: "5° ~ 15°" },
+    { key: "gt15", label: "> 15°" }
+];
+const slopeKey = ref("5to15");
+
+const getDefaultPoint = () =>
+    localStorage.getItem("MINI_USER_LOCATION_POINT") ||
+    store.state.home.miniUserLocationPoint ||
+    DEFAULT_POINT;
+
+const initMap = () => {
+    if (!mapRef.value || !pointWkt.value) return;
+    if (!previewMap) {
+        previewMap = new SelectLocationMap({ iconSrc: POINT_ICON.plant });
+        previewMap.initMap(pointWkt.value, mapRef.value, {
+            enableClick: false,
+            showPoint: true,
+            padding: [40, 0, 10, 0]
+        });
+    } else if (previewMap.kmap) {
+        const match = String(pointWkt.value).match(/POINT\s*\(([\d.\-]+)\s+([\d.\-]+)\)/i);
+        if (match) {
+            previewMap.showPoint([Number(match[1]), Number(match[2])]);
+        }
+    }
+    nextTick(() => {
+        previewMap?.kmap?.map?.updateSize?.();
+    });
+};
+
+const open = (payload = {}) => {
+    pointWkt.value = payload.point || getDefaultPoint();
+    if (payload.slopeKey && slopeList.some((item) => item.key === payload.slopeKey)) {
+        slopeKey.value = payload.slopeKey;
+    }
+    visible.value = true;
+};
+
+const close = () => {
+    visible.value = false;
+};
+
+const handleOpened = () => {
+    nextTick(() => initMap());
+};
+
+const handleClosed = () => {
+    previewMap?.clearLayer?.();
+};
+
+/** 点击地图可跳转选点页(后续接入) */
+const handleMapClick = () => {};
+
+const handleConfirm = () => {
+    if (!slopeKey.value) {
+        showToast("请选择坡度信息");
+        return;
+    }
+    const slope = slopeList.find((item) => item.key === slopeKey.value);
+    emit("confirm", {
+        point: pointWkt.value,
+        slopeKey: slopeKey.value,
+        slopeLabel: slope?.label || ""
+    });
+    close();
+};
+
+defineExpose({ open, close });
+</script>
+
+<style lang="scss" scoped>
+.edit-plant-point-popup {
+    width: 90%;
+    padding: 20px 16px;
+    box-sizing: border-box;
+    background: linear-gradient(360deg, #ffffff 74.2%, #d1ebff 100%);
+    border-radius: 8px;
+
+    .popup-title {
+        margin-bottom: 14px;
+        font-size: 16px;
+        font-weight: 500;
+        color: #000;
+        line-height: 1.3;
+    }
+
+    .map-preview {
+        width: 100%;
+        height: 180px;
+        border-radius: 8px;
+        overflow: hidden;
+        background: #e8eef3;
+    }
+
+    .popup-section {
+        margin-top: 16px;
+
+        &__title {
+            margin-bottom: 12px;
+            font-size: 16px;
+            color: #000;
+        }
+    }
+
+    .slope-row {
+        display: grid;
+        grid-template-columns: repeat(3, 1fr);
+        gap: 10px;
+    }
+
+    .slope-tag {
+        height: 40px;
+        line-height: 40px;
+        text-align: center;
+        border-radius: 4px;
+        font-size: 15px;
+        color: #000;
+        background: #f7f7f7;
+        border: 1px solid transparent;
+        box-sizing: border-box;
+
+        &.selected {
+            color: #2199f8;
+            background: rgba(33, 153, 248, 0.1);
+            border-color: #2199f8;
+        }
+    }
+
+    .popup-confirm {
+        margin-top: 24px;
+        height: 44px;
+        line-height: 44px;
+        text-align: center;
+        border-radius: 22px;
+        background: #2199f8;
+        color: #fff;
+        font-size: 16px;
+    }
+}
+</style>

+ 18 - 0
src/views/old_mini/agri_file/pages/diagnosisReport.vue

@@ -72,26 +72,31 @@
             @confirm="handleRestingDateConfirm"
             @confirm="handleRestingDateConfirm"
             @switchGrowing="handleSwitchGrowing"
             @switchGrowing="handleSwitchGrowing"
         />
         />
+        <editPlantPointPopup ref="editPlantPointPopupRef" @confirm="handlePlantPointConfirm" />
     </div>
     </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
 import { computed, ref } from "vue";
 import { computed, ref } from "vue";
+import { useRouter } from "vue-router";
 import { Plus, Edit } from "@element-plus/icons-vue";
 import { Plus, Edit } from "@element-plus/icons-vue";
 import adjustPhenologyPopup from "../components/adjustPhenologyPopup.vue";
 import adjustPhenologyPopup from "../components/adjustPhenologyPopup.vue";
 import restingDatePopup from "../components/restingDatePopup.vue";
 import restingDatePopup from "../components/restingDatePopup.vue";
+import editPlantPointPopup from "../components/editPlantPointPopup.vue";
 import tab1Icon from "@/assets/img/report/tab-1.png";
 import tab1Icon from "@/assets/img/report/tab-1.png";
 import tab2Icon from "@/assets/img/report/tab-2.png";
 import tab2Icon from "@/assets/img/report/tab-2.png";
 import tab3Icon from "@/assets/img/report/tab-3.png";
 import tab3Icon from "@/assets/img/report/tab-3.png";
 import tab4Icon from "@/assets/img/report/tab-4.png";
 import tab4Icon from "@/assets/img/report/tab-4.png";
 import tab5Icon from "@/assets/img/report/tab-5.png";
 import tab5Icon from "@/assets/img/report/tab-5.png";
 
 
+const router = useRouter();
 const varietyList = ref([
 const varietyList = ref([
     { id: 1, name: "金柚" }
     { id: 1, name: "金柚" }
 ]);
 ]);
 const activeVarietyId = ref(1);
 const activeVarietyId = ref(1);
 const adjustPhenologyPopupRef = ref(null);
 const adjustPhenologyPopupRef = ref(null);
 const restingDatePopupRef = ref(null);
 const restingDatePopupRef = ref(null);
+const editPlantPointPopupRef = ref(null);
 
 
 /** 基本信息展示数据:后续可替换为接口 */
 /** 基本信息展示数据:后续可替换为接口 */
 const baseInfoList = ref([
 const baseInfoList = ref([
@@ -144,6 +149,14 @@ const handleBaseAction = (key) => {
         adjustPhenologyPopupRef.value?.open();
         adjustPhenologyPopupRef.value?.open();
         return;
         return;
     }
     }
+    if (key === "editLocation") {
+        editPlantPointPopupRef.value?.open();
+        return;
+    }
+    if (key === "soilSense") {
+        router.push("/soil_sense");
+        return;
+    }
     // 后续接入对应弹窗 / 跳转
     // 后续接入对应弹窗 / 跳转
     console.log("base action:", key);
     console.log("base action:", key);
 };
 };
@@ -161,6 +174,11 @@ const handleRestingDateConfirm = (payload) => {
     // 后续接入接口保存
     // 后续接入接口保存
     console.log("resting date confirm:", payload);
     console.log("resting date confirm:", payload);
 };
 };
+
+const handlePlantPointConfirm = (payload) => {
+    // 后续接入接口保存
+    console.log("plant point confirm:", payload);
+};
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>

+ 337 - 0
src/views/old_mini/agri_file/pages/soilSense.vue

@@ -0,0 +1,337 @@
+<template>
+    <div class="soil-sense-page">
+        <custom-header name="土壤感知" :isGoBack="true" @goback="handleBack" />
+        <div class="soil-sense-body">
+            <div class="soil-hero">
+                <div class="soil-hero__top">
+                    <div class="soil-hero__main">
+                        <div class="soil-hero__title-row">
+                            <span class="soil-hero__title">{{ theme }}</span>
+                            <span class="soil-hero__tag soil-hero__tag--level">
+                                <el-icon><BellFilled /></el-icon>
+                                {{ level }}
+                            </span>
+                            <span class="soil-hero__tag soil-hero__tag--zone">{{ zoneName }}</span>
+                        </div>
+                        <div class="soil-hero__reason">{{ subtitle }}</div>
+                    </div>
+                    <div class="soil-hero__share" @click="handleShare">
+                        <Icon name="share" />
+                        <span>{{ t("agriFile.forward") }}</span>
+                    </div>
+                </div>
+            </div>
+
+            <div
+                v-for="(block, index) in soilBlocks"
+                :key="block.key"
+                class="soil-main"
+                :class="{ 'soil-main--follow': index > 0 }"
+            >
+                <div class="soil-section-title">
+                    <img class="soil-section-title__icon" src="@/assets/img/report/qust-icon.png" alt="" />
+                    <span>{{ block.title }}</span>
+                </div>
+
+                <div class="interact-box">
+                    <div class="interact-box__head">
+                        <span class="interact-box__badge">{{ t("agriFile.interactQuestion") }}</span>
+                        <span class="interact-box__question">{{ block.question }}</span>
+                    </div>
+                    <div class="interact-box__options">
+                        <div
+                            v-for="item in block.options"
+                            :key="item.id"
+                            class="soil-option"
+                            :class="{ selected: block.selectedId === item.id }"
+                            @click="block.selectedId = item.id"
+                        >
+                            {{ item.name }}
+                        </div>
+                    </div>
+                </div>
+
+                <div class="guide-block">
+                    <div class="guide-title">{{ t("agriFile.patrolGuide") }}</div>
+                    <photo-provider :photo-closable="true">
+                        <photo-consumer :src="block.guideImage">
+                            <img class="guide-block__img" :src="block.guideImage" alt="" />
+                        </photo-consumer>
+                    </photo-provider>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { useRouter } from "vue-router";
+import { Icon } from "vant";
+import { ElMessage } from "element-plus";
+import { BellFilled } from "@element-plus/icons-vue";
+import customHeader from "@/components/customHeader.vue";
+import { useI18n } from "@/i18n";
+
+const { t } = useI18n();
+const router = useRouter();
+
+const theme = ref(t("agriFile.interactTheme"));
+const subtitle = ref("互动小标题");
+const level = ref(t("agriFile.riskLevel2"));
+const zoneName = ref(t("agriFile.zoneOne"));
+const defaultGuideImage = require("@/assets/img/agricultural/patrol-guide.png");
+
+/** 土壤感知模块:后续可替换为接口 */
+const soilBlocks = ref([
+    {
+        key: "texture",
+        title: "土壤质地判断",
+        question: "请选择您的土壤质地",
+        selectedId: 2,
+        guideImage: defaultGuideImage,
+        options: [
+            { id: 1, name: "砂土" },
+            { id: 2, name: "砂壤土" },
+            { id: 3, name: "壤土" },
+            { id: 4, name: "黏壤土" },
+            { id: 5, name: "黏土" }
+        ]
+    },
+    {
+        key: "ph",
+        title: "土壤酸碱度判断",
+        question: "请选择您的土壤酸碱度",
+        selectedId: 2,
+        guideImage: defaultGuideImage,
+        options: [
+            { id: 1, name: "过酸" },
+            { id: 2, name: "弱酸" },
+            { id: 3, name: "中性" },
+            { id: 4, name: "弱碱性" },
+            { id: 5, name: "过碱" }
+        ]
+    }
+]);
+
+const handleBack = () => {
+    router.back();
+};
+
+const handleShare = () => {
+    ElMessage.info(t("agriFile.forwardTip"));
+};
+</script>
+
+<style lang="scss" scoped>
+.soil-sense-page {
+    width: 100%;
+    height: 100vh;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    background: #f5f5f5;
+    box-sizing: border-box;
+
+    .soil-sense-body {
+        flex: 1;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+        padding-bottom: 20px;
+    }
+
+    .soil-hero {
+        padding: 18px 10px 38px;
+        background: #2199f8;
+        color: #fff;
+
+        &__top {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+        }
+
+        &__title-row {
+            display: flex;
+            align-items: center;
+            flex-wrap: wrap;
+            gap: 8px;
+        }
+
+        &__title {
+            font-size: 28px;
+            font-family: "PangMenZhengDao", "pangmenzhengdao", sans-serif;
+            margin-top: -5px;
+        }
+
+        &__reason {
+            margin-top: 10px;
+            padding: 4px 10px;
+            border-radius: 4px;
+            background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%);
+            font-size: 14px;
+        }
+
+        &__tag {
+            display: inline-flex;
+            align-items: center;
+            gap: 4px;
+            padding: 1px 4px;
+            border-radius: 2px;
+            background: #fff;
+            font-size: 12px;
+        }
+
+        &__tag--level {
+            color: #ff6a6a;
+        }
+
+        &__tag--zone {
+            color: #2199f8;
+        }
+
+        &__share {
+            flex: none;
+            display: flex;
+            align-items: center;
+            gap: 4px;
+            padding: 4px 14px;
+            border-radius: 25px;
+            background: rgba(19, 125, 209, 0.5);
+            color: #fff;
+        }
+    }
+
+    .soil-main {
+        position: relative;
+        z-index: 1;
+        margin: -24px 10px 0;
+        padding: 14px 12px 16px;
+        border-radius: 10px;
+        background: #fff;
+
+        &--follow {
+            margin: 10px 10px 0;
+        }
+    }
+
+    .soil-section-title {
+        display: flex;
+        align-items: center;
+        gap: 6px;
+        margin-bottom: 12px;
+        padding-bottom: 10px;
+        border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+        color: #2199f8;
+        font-size: 16px;
+        font-weight: 500;
+
+        &__icon {
+            width: 20px;
+            height: 20px;
+        }
+    }
+
+    .interact-box {
+        padding: 12px;
+        border-radius: 8px;
+        background: rgba(33, 153, 248, 0.1);
+
+        &__head {
+            display: flex;
+            align-items: center;
+            flex-wrap: wrap;
+            gap: 8px;
+            margin-bottom: 12px;
+        }
+
+        &__badge {
+            display: inline-flex;
+            align-items: center;
+            padding: 2px 8px;
+            border-radius: 4px;
+            background: #2199f8;
+            color: #fff;
+            font-size: 12px;
+            line-height: 18px;
+        }
+
+        &__question {
+            font-size: 14px;
+            font-weight: 500;
+            color: #1d2129;
+        }
+
+        &__options {
+            display: grid;
+            grid-template-columns: repeat(3, 1fr);
+            gap: 8px;
+        }
+    }
+
+    .soil-option {
+        position: relative;
+        height: 36px;
+        line-height: 36px;
+        text-align: center;
+        border-radius: 6px;
+        background: #fff;
+        border: 1px solid transparent;
+        font-size: 14px;
+        color: #666;
+        box-sizing: border-box;
+
+        &.selected {
+            color: #2199f8;
+            border-color: #2199f8;
+
+            &::after {
+                content: "";
+                position: absolute;
+                top: -1px;
+                right: -1px;
+                width: 18px;
+                height: 14px;
+                background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
+            }
+        }
+    }
+
+    .guide-block {
+        margin-top: 10px;
+        border: 1px solid rgba(33, 153, 248, 0.2);
+        background: linear-gradient(360deg, #FFFFFF 90.33%, #ABD8FC 116.61%, #2199F8 148.55%);
+        border-radius: 8px;
+        padding: 10px;
+    }
+
+    .guide-title {
+        position: relative;
+        z-index: 1;
+        display: inline-block;
+        font-size: 16px;
+        font-weight: 500;
+        color: #1d2129;
+
+        &::after {
+            content: "";
+            position: absolute;
+            left: 0;
+            bottom: 2px;
+            z-index: -1;
+            width: 90px;
+            height: 6px;
+            border-radius: 25px;
+            background: linear-gradient(90deg, #2199f8 0%, rgba(255, 255, 255, 0.37) 100%);
+            opacity: 0.4;
+        }
+    }
+
+    .guide-block__img {
+        display: block;
+        width: 100%;
+        margin-top: 8px;
+        border-radius: 8px;
+    }
+}
+</style>