Pārlūkot izejas kodu

feat:添加物候弹窗组件和逻辑

wangsisi 4 dienas atpakaļ
vecāks
revīzija
51749b99c6

+ 97 - 19
src/components/pageComponents/GrowthStageTimeline.vue

@@ -51,9 +51,19 @@
                         <div class="growth-stage-timeline__handle-core">
                             <div
                                 v-if="showHandleTooltip"
-                                class="growth-stage-timeline__tooltip"
+                                class="growth-stage-timeline__tooltip-wrap"
                             >
-                                {{ tooltipText }}
+                                <div
+                                    ref="tooltipBubbleRef"
+                                    class="growth-stage-timeline__tooltip"
+                                    :style="tooltipBubbleStyle"
+                                >
+                                    {{ tooltipText }}
+                                </div>
+                                <div
+                                    class="growth-stage-timeline__tooltip-caret"
+                                    aria-hidden="true"
+                                ></div>
                             </div>
                             <div class="growth-stage-timeline__handle-body">
                                 <span class="growth-stage-timeline__handle-bar"></span>
@@ -145,6 +155,15 @@ const emit = defineEmits(["update:modelValue", "change"]);
 const scrollRef = ref(null);
 const trackRef = ref(null);
 const handleRef = ref(null);
+const tooltipBubbleRef = ref(null);
+
+/** 气泡相对手柄水平中心的额外偏移(px),用于避免贴边时被 overflow 裁切 */
+const tooltipShiftPx = ref(0);
+
+const TOOLTIP_EDGE_MARGIN_PX = 8;
+
+/** 本次进入页面内展示;拖动手柄产生位移后关闭,下次路由/页面再进入会重新挂载并再次显示 */
+const showHandleTooltip = ref(true);
 
 const normalizedStages = computed(() =>
     Array.isArray(props.stages) ? props.stages : []
@@ -159,7 +178,7 @@ const gridCols = computed(() =>
 );
 
 /** 左右留白:手柄 translate(-50%) 与气泡在首尾否则会溢出滚动宽度,且气泡换行会拉高整列导致视觉上“掉下去” */
-const INNER_EDGE_PAD_PX = 24;
+const INNER_EDGE_PAD_PX = 12;
 
 const innerStyle = computed(() => ({
     boxSizing: "border-box",
@@ -293,19 +312,62 @@ watch(
         handleRatio.value = indexToRatio(activeIndex.value);
         nextTick(() => {
             scrollToCenterRatio(indexToRatio(activeIndex.value), "auto");
+            clampTooltipToScrollArea();
         });
     },
     { immediate: true }
 );
 
+watch(showHandleTooltip, (v) => {
+    if (v) clampTooltipToScrollArea();
+});
+
+watch(
+    () => handleRatio.value,
+    () => {
+        if (showHandleTooltip.value) clampTooltipToScrollArea();
+    }
+);
+
 let resizeObserver = null;
 
 const handleStyle = computed(() => ({
     left: `${handleRatio.value * 100}%`,
 }));
 
-/** 本次进入页面内展示;拖动手柄产生位移后关闭,下次路由/页面再进入会重新挂载并再次显示 */
-const showHandleTooltip = ref(true);
+const tooltipBubbleStyle = computed(() => ({
+    transform: `translateX(${tooltipShiftPx.value}px)`,
+}));
+
+/**
+ * 以滚动可视区为边界,将气泡水平平移最小距离使其完整可见;箭头留在手柄中心(见模板结构)。
+ */
+function clampTooltipToScrollArea() {
+    if (!showHandleTooltip.value) return;
+    const scrollEl = scrollRef.value;
+    const bubbleEl = tooltipBubbleRef.value;
+    if (!scrollEl || !bubbleEl) return;
+
+    tooltipShiftPx.value = 0;
+    nextTick(() => {
+        requestAnimationFrame(() => {
+            const b = scrollEl.getBoundingClientRect();
+            const t = bubbleEl.getBoundingClientRect();
+            const pad = TOOLTIP_EDGE_MARGIN_PX;
+            const sLow = b.left + pad - t.left;
+            const sHigh = b.right - pad - t.right;
+            let s = 0;
+            if (sLow <= sHigh) {
+                if (sLow <= 0 && sHigh >= 0) s = 0;
+                else if (sLow > 0) s = sLow;
+                else s = sHigh;
+            } else {
+                s = (sLow + sHigh) / 2;
+            }
+            tooltipShiftPx.value = s;
+        });
+    });
+}
 
 let dragging = false;
 let activePointerId = null;
@@ -344,6 +406,7 @@ function onPointerMove(e) {
     movedDuringHandleDrag = true;
     handleRatio.value = clientXToRatio(e.clientX);
     syncScrollToRatio(handleRatio.value);
+    if (showHandleTooltip.value) clampTooltipToScrollArea();
 }
 
 function onPointerUp(e) {
@@ -367,12 +430,19 @@ function onPointerUp(e) {
     emit("change", nextIdx, normalizedStages.value[nextIdx]);
 }
 
+function onScrollAreaScroll() {
+    if (showHandleTooltip.value) clampTooltipToScrollArea();
+}
+
 onMounted(() => {
     window.addEventListener("pointermove", onPointerMove);
     window.addEventListener("pointerup", onPointerUp);
     window.addEventListener("pointercancel", onPointerUp);
+    window.addEventListener("resize", onScrollAreaScroll);
     const el = scrollRef.value;
+    el?.addEventListener("scroll", onScrollAreaScroll, { passive: true });
     el?.addEventListener("wheel", onTimelineWheel, { passive: false });
+    nextTick(() => clampTooltipToScrollArea());
     el?.addEventListener("touchstart", onScrollAreaTouchStart, {
         passive: true,
     });
@@ -387,6 +457,7 @@ onMounted(() => {
                           indexToRatio(activeIndex.value),
                           "auto"
                       );
+                      clampTooltipToScrollArea();
                   });
               })
             : null;
@@ -399,7 +470,9 @@ onBeforeUnmount(() => {
     window.removeEventListener("pointermove", onPointerMove);
     window.removeEventListener("pointerup", onPointerUp);
     window.removeEventListener("pointercancel", onPointerUp);
+    window.removeEventListener("resize", onScrollAreaScroll);
     const el = scrollRef.value;
+    el?.removeEventListener("scroll", onScrollAreaScroll);
     el?.removeEventListener("wheel", onTimelineWheel);
     el?.removeEventListener("touchstart", onScrollAreaTouchStart);
     el?.removeEventListener("touchmove", onScrollAreaTouchMove);
@@ -533,14 +606,22 @@ onBeforeUnmount(() => {
     align-items: center;
 }
 
-.growth-stage-timeline__tooltip {
+.growth-stage-timeline__tooltip-wrap {
     position: absolute;
     left: 50%;
     bottom: 100%;
     transform: translateX(-50%);
-    max-width: 220px;
-    white-space: nowrap;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
     margin-bottom: 6px;
+    pointer-events: none;
+    z-index: 3;
+}
+
+.growth-stage-timeline__tooltip {
+    max-width: min(220px, calc(100vw - 24px));
+    white-space: nowrap;
     padding: 6px 10px;
     border-radius: 6px;
     background: #808080;
@@ -549,18 +630,15 @@ onBeforeUnmount(() => {
     line-height: 1.4;
     text-align: center;
     box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
-    pointer-events: none;
+}
 
-    &::after {
-        content: "";
-        position: absolute;
-        left: 50%;
-        bottom: -5px;
-        transform: translateX(-50%);
-        border-width: 5px 5px 0 5px;
-        border-style: solid;
-        border-color: #808080 transparent transparent transparent;
-    }
+.growth-stage-timeline__tooltip-caret {
+    width: 0;
+    height: 0;
+    margin-top: -1px;
+    border-width: 5px 5px 0 5px;
+    border-style: solid;
+    border-color: #808080 transparent transparent transparent;
 }
 
 .growth-stage-timeline__handle-body {

+ 136 - 0
src/components/popup/UploadProgressPopup.vue

@@ -0,0 +1,136 @@
+<template>
+    <popup :show="show" round :close-on-click-overlay="false" class="upload-progress-popup"
+        @update:show="emit('update:show', $event)">
+        <slot name="header"></slot>
+        <div class="upload-box" v-loading="popupImageUploadLoading" element-loading-text="上传中...">
+            <div class="box-header">
+                <div class="upload-title">
+                    <span>上传照片</span>
+                    <span class="optional">(可选)</span>
+                </div>
+                <div class="ai-btn">AI 智能分析</div>
+            </div>
+            <upload ref="uploadRef" :maxCount="10" :initImgArr="initImgArr" @handleUpload="onHandleUpload">
+            </upload>
+            <div class="upload-result">AI识别结果:该病为该病为该病为该病为病为该病为病为该病为</div>
+        </div>
+        <div class="upload-action-btns">
+            <div class="cancel-btn" @click="emit('cancel')">取消</div>
+            <div class="confirm-btn" @click="emit('confirm')">{{ confirmText }}</div>
+        </div>
+    </popup>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { Popup } from 'vant';
+import upload from '@/components/upload.vue';
+
+defineProps({
+    show: {
+        type: Boolean,
+        default: false,
+    },
+    popupImageUploadLoading: {
+        type: Boolean,
+        default: false,
+    },
+    initImgArr: {
+        type: Array,
+        default: () => [],
+    },
+    confirmText: {
+        type: String,
+        default: '确认信息',
+    },
+});
+
+const emit = defineEmits(['update:show', 'cancel', 'confirm', 'handleUpload']);
+
+const uploadRef = ref(null);
+
+function onHandleUpload(payload) {
+    emit('handleUpload', payload);
+}
+
+function uploadReset() {
+    uploadRef.value?.uploadReset?.();
+}
+
+defineExpose({
+    uploadReset,
+});
+</script>
+
+<style scoped lang="scss">
+.upload-progress-popup {
+    width: 100%;
+    padding: 24px 16px;
+    background: linear-gradient(360deg, #FFFFFF 74.2%, #D1EBFF 100%);
+
+    .upload-box {
+        margin-bottom: 12px;
+        position: relative;
+        min-height: 88px;
+
+        .box-header {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            margin-bottom: 12px;
+
+            .upload-title {
+                font-size: 16px;
+
+                .optional {
+                    font-size: 12px;
+                    color: rgba(18, 18, 18, 0.2);
+                }
+            }
+
+            .ai-btn {
+                padding: 5px 10px;
+                border-radius: 4px;
+                background: rgba(33, 153, 248, 0.1);
+                color: #2199F8;
+                border: 1px solid #2199F8;
+                opacity: 0.5;
+            }
+        }
+
+        .upload-result {
+            color: #646464;
+            padding: 6px 10px;
+            background: rgba(161, 161, 161, 0.1);
+            border-radius: 5px;
+            margin-top: 12px;
+        }
+    }
+
+    .upload-action-btns {
+        display: flex;
+        gap: 10px;
+        margin-top: 16px;
+
+        .cancel-btn,
+        .confirm-btn {
+            flex: 1;
+            border-radius: 4px;
+            padding: 8px;
+            text-align: center;
+            font-size: 16px;
+        }
+
+        .cancel-btn {
+            border: 1px solid #dcdfe6;
+            color: #606266;
+            background: #ffffff;
+        }
+
+        .confirm-btn {
+            background: #2199f8;
+            color: #ffffff;
+        }
+    }
+}
+</style>

+ 0 - 495
src/components/popup/activeUploadPopup.vue

@@ -1,495 +0,0 @@
-<template>
-    <popup
-        class="active-upload-popup"
-        v-model:show="show"
-        closeable
-        teleport="body"
-        :overlay-style="{ 'z-index': 9999 }"
-        :close-on-click-overlay="false"
-        @closed="handleClosed"
-    >
-        <div class="header" v-if="selectCurrentPhenology">
-            <div class="title">
-                <span class="required">*</span>
-                当前物候期
-            </div>
-            <div class="date-input">
-                <el-select
-                    popper-class="custom-select-dropdown"
-                    v-model="currentPhenologyId"
-                    size="large"
-                    placeholder="请选择当前物候期"
-                >
-                    <el-option
-                        v-for="item in phenologyList"
-                        :key="item.id"
-                        :label="item.name"
-                        :value="item.id"
-                    ></el-option>
-                </el-select>
-            </div>
-        </div>
-        <div class="header">
-            <div class="title">
-                <span class="required">*</span>
-                {{ problemTitle }}
-            </div>
-            <div class="date-input">
-                <el-date-picker
-                    v-model="uploadDate"
-                    size="large"
-                    popper-class="custom-select-dropdown"
-                    style="width: 100%"
-                    type="date"
-                    placeholder="请选择日期"
-                    :editable="false"
-                />
-            </div>
-        </div>
-        <div v-if="needExecutor">
-            <div class="header">
-                <div class="title">
-                    <span class="required">*</span>
-                    请确认执行人
-                </div>
-                <div class="date-input">
-                    <el-select
-                        popper-class="custom-select-dropdown"
-                        size="large"
-                        v-model="executorId"
-                        placeholder="请选择执行人"
-                    >
-                        <el-option
-                            v-for="(item, index) in executorList"
-                            :key="index"
-                            :label="item.name"
-                            :value="item.miniUserId"
-                        >
-                            <span>{{ item.name }}</span>
-                            <span class="popup-role-text" :class="'role-' + item.role">
-                                {{ roleMap[item.role] }}
-                            </span>
-                        </el-option>
-                    </el-select>
-                </div>
-            </div>
-            <div class="header flex-header">
-                <div class="title">
-                    <span class="required">*</span>
-                    是否需要复核?
-                </div>
-                <div class="date-input">
-                    <el-radio-group v-model="needReview">
-                        <el-radio :value="1">需要</el-radio>
-                        <el-radio :value="0">不需要</el-radio>
-                    </el-radio-group>
-                </div>
-            </div>
-            <div class="header" v-if="needReview">
-                <div class="title">
-                    <span class="required">*</span>
-                    请选择农事完成后的复核时间
-                </div>
-                <div class="date-input review-day-input">
-                    <el-input size="large" v-model="reviewDay" type="number" step="0.01">
-                        <template #append>天后</template>
-                    </el-input>
-                </div>
-            </div>
-        </div>
-        <div class="tips-text img-desc" v-if="imgDesc">
-            <span class="required" v-if="userInfoObj?.agriculturalRole !== 1">*</span>{{ imgDesc }}
-        </div>
-        <div class="tips-text" v-else>上传照片,诊断更准确哦~</div>
-        <upload :textShow="true" class="upload-wrap" exampleImg>
-            <img class="example" src="@/assets/img/home/example-4.png" alt="" />
-            <img class="example" src="@/assets/img/home/plus.png" alt="" />
-        </upload>
-        <div class="btn" :class="{ disabled: isUploading }" @click="handleUpload">
-            {{ isUploading ? "提交中..." : "确认" }}
-        </div>
-    </popup>
-
-    <!-- 上传成功提示弹窗 -->
-    <popup class="success-popup" v-model:show="successShow" :close-on-click-overlay="false">
-        <div class="success-wrap">
-            <img class="success-icon" src="@/assets/img/home/right.png" alt="" />
-            <div class="success-title">好的,感谢您的配合</div>
-            <div class="success-sub">请您耐心等待农事确认</div>
-            <div class="btn" @click="successShow = false">我知道了</div>
-        </div>
-    </popup>
-
-    <tip-popup
-        v-model:show="showTipPopup"
-        type="warning"
-        text="请完善"
-        text2="信息"
-        highlightText="处方报价"
-        :overlayStyle="{ 'z-index': 9999 }"
-        buttonText="去完善"
-        @confirm="handleBtn"
-        :hasClose="true"
-        :closeOnClickOverlay="false"
-    />
-</template>
-
-<script setup>
-import { Popup } from "vant";
-import { onMounted, onUnmounted, ref } from "vue";
-import upload from "@/components/upload";
-import eventBus from "@/api/eventBus";
-import { ElMessage } from "element-plus";
-import { useRouter } from "vue-router";
-import tipPopup from "@/components/popup/tipPopup.vue";
-
-const router = useRouter();
-const show = ref(false);
-const gardenId = ref(null);
-const images = ref([]);
-const uploadDate = ref("");
-const problemTitle = ref("请选择问题");
-const successShow = ref(false);
-const isUploading = ref(false); // 标记是否正在上传中
-const showTipPopup = ref(false);
-onMounted(() => {
-    eventBus.off("upload:changeArr", uploadChange);
-    eventBus.on("upload:changeArr", uploadChange);
-    eventBus.on("activeUpload:show", handleShow);
-    eventBus.on("activeUpload:success", handleSuccess);
-});
-
-const userInfo = localStorage.getItem("localUserInfo");
-const userInfoObj = userInfo ? JSON.parse(userInfo) : {};
-
-function uploadChange(arr) {
-    images.value = arr;
-}
-
-function formatDate(date) {
-    let year = date.getFullYear();
-    let month = String(date.getMonth() + 1).padStart(2, "0");
-    let day = String(date.getDate()).padStart(2, "0");
-    return `${year}-${month}-${day}`;
-}
-
-const type = ref(null);
-const arrangeId = ref(null);
-// 选择执行人
-const needExecutor = ref(false);
-const executorList = ref([]);
-const executorId = ref(null);
-const needReview = ref(null);
-const reviewDay = ref(null);
-const roleMap = ref({
-    1: "超级管理员",
-    2: "项目管理员",
-    3: "普通成员",
-});
-// 选择当前物候期
-const currentPhenologyId = ref(null);
-const phenologyList = ref([]);
-const selectCurrentPhenology = ref(false);
-
-// 图片上传标题描述
-const imgDesc = ref(null);
-function handleShow({
-    gardenIdVal,
-    problemTitleVal,
-    typeVal,
-    arrangeIdVal,
-    executorListVal,
-    imgDescVal,
-    needExecutorVal,
-    selectCurrentPhenologyVal,
-    phenologyListVal,
-    farmWorkIdVal,
-    schemeIdVal,
-}) {
-    images.value = [];
-    gardenId.value = gardenIdVal;
-    problemTitle.value = problemTitleVal || "请选择问题";
-    uploadDate.value = new Date();
-    show.value = true;
-    type.value = typeVal;
-    arrangeId.value = arrangeIdVal;
-    executorList.value = executorListVal;
-    imgDesc.value = imgDescVal;
-    needExecutor.value = needExecutorVal;
-    selectCurrentPhenology.value = selectCurrentPhenologyVal ? true : false;
-    phenologyList.value = phenologyListVal;
-    // 重置上传状态
-    isUploading.value = false;
-    if (typeVal !== "question") {
-        // 如果没有报价信息,则跳转去完善报价信息
-        VE_API.monitor.validatePesticideFertilizerQuotes({ id: farmWorkIdVal, schemeId: schemeIdVal }).then((res) => {
-            if (res.data === false) {
-                ids.value = {
-                    farmWorkId: farmWorkIdVal,
-                    schemeId: schemeIdVal,
-                    farmId: gardenIdVal,
-                    id: arrangeIdVal,
-                };
-                showTipPopup.value = true;
-            }
-        });
-    }
-}
-
-const ids = ref({});
-function handleBtn() {
-    router.push(
-        `/modify?farmWorkId=${ids.value.farmWorkId}&schemeId=${ids.value.schemeId}&farmId=${ids.value.farmId}&id=${ids.value.id}&onlyPrice=true&isEdit=true`
-    );
-    showTipPopup.value = false;
-}
-
-function handleSuccess() {
-    successShow.value = true;
-}
-
-const emit = defineEmits(["handleUploadSuccess"]);
-
-const handleUpload = () => {
-    // 如果正在上传中,直接返回,防止重复调用
-    if (isUploading.value) return;
-
-    // 执行人必选
-    if (needExecutor.value && (!executorId.value || executorId.value === null)) {
-        return ElMessage.warning("请选择执行人");
-    }
-    // 是否需要复核必选
-    if (needExecutor.value && (needReview.value === null)) {
-        return ElMessage.warning("请选择是否需要复核");
-    }
-    // 如果选了需要复核,复核天数必填
-    if (needExecutor.value && needReview.value === 1 && (!reviewDay.value || reviewDay.value === null || reviewDay.value === '')) {
-        return ElMessage.warning("请填写复核天数");
-    }
-    if (userInfoObj?.agriculturalRole !== 1 && images.value.length === 0) return ElMessage.warning("请上传图片");
-    
-    
-    let paramsObj = {
-        farmId: gardenId.value,
-        arrangeId: arrangeId.value,
-        executeDate: formatDate(uploadDate.value),
-        imagePaths: images.value,
-    };
-    if (needExecutor.value) {
-        paramsObj = {
-            ...paramsObj,
-            executeDeadlineDate: formatDate(uploadDate.value),
-            executeDate: null,
-            executorUserId: executorId.value,
-            needReview: needReview.value,
-            reviewIntervalDays: reviewDay.value,
-        };
-    }
-    if (type.value === "question") {
-        show.value = false;
-        emit("handleUploadSuccess", paramsObj);
-        return;
-    }
-    triggerFarmWork(paramsObj, true);
-};
-
-function triggerFarmWork(paramsObj, showSuccess) {
-    // 如果正在上传中,直接返回,防止重复调用
-    if (isUploading.value) return;
-
-    // 设置上传状态为 true
-    isUploading.value = true;
-
-    VE_API.monitor
-        .triggerFarmWork(paramsObj)
-        .then((res) => {
-            if (res.code === 0) {
-                if (showSuccess) {
-                    show.value = false;
-                    // successShow.value = true;
-                    ElMessage.success("农事已转入成功");
-                    emit("handleUploadSuccess", paramsObj);
-                }
-            }
-        })
-        .catch((error) => {
-            console.error("触发农事失败:", error);
-        })
-        .finally(() => {
-            // 无论成功或失败,都重置上传状态
-            isUploading.value = false;
-        });
-}
-
-function showPopup(data) {
-    handleShow(data);
-}
-
-defineExpose({
-    triggerFarmWork,
-    showPopup,
-});
-
-function handleClosed() {
-    eventBus.emit("upload:reset");
-    // 清除所有数据
-    images.value = [];
-    // uploadDate.value = "";
-    executorId.value = null;
-    needReview.value = null;
-    reviewDay.value = null;
-    // currentPhenologyId.value = null;
-    // gardenId.value = null;
-    // problemTitle.value = "请选择问题";
-    // type.value = null;
-    // arrangeId.value = null;
-    // executorList.value = [];
-    // imgDesc.value = null;
-    // needExecutor.value = false;
-    // selectCurrentPhenology.value = false;
-    // phenologyList.value = [];
-    // isUploading.value = false;
-    // successShow.value = false;
-}
-
-onUnmounted(() => {
-    eventBus.off("activeUpload:show", handleShow);
-    eventBus.off("activeUpload:success", handleSuccess);
-    eventBus.off("upload:changeArr", uploadChange);
-    show.value = false;
-});
-</script>
-
-<style lang="scss" scoped>
-.active-upload-popup {
-    z-index: 9999 !important;
-    width: 90%;
-    box-sizing: border-box;
-    padding: 24px 18px 20px;
-    background: linear-gradient(0deg, #ffffff 70%, #d1ebff 100%);
-    border-radius: 10px;
-    ::v-deep {
-        .van-popup__close-icon {
-            color: #000;
-        }
-    }
-    .header {
-        .title {
-            font-size: 16px;
-            font-weight: 500;
-            display: flex;
-        }
-        align-items: center;
-        .date-input {
-            margin: 12px 0;
-            ::v-deep {
-                .el-input__inner {
-                    caret-color: transparent;
-                }
-            }
-        }
-        .review-day-input {
-            border: 1px solid #dcdcdc;
-            border-radius: 3px;
-            ::v-deep {
-                .el-input__wrapper {
-                    width: 40px;
-                    flex: none;
-                    box-shadow: none;
-                }
-                .el-input-group__append {
-                    box-shadow: none;
-                    background: none;
-                }
-            }
-        }
-    }
-    .flex-header {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        margin-bottom: 12px;
-        .date-input {
-            margin: 0;
-        }
-    }
-    .required {
-        color: #ff4d4f;
-        margin-right: 4px;
-    }
-    .tips-text {
-        font-weight: 500;
-        &.img-desc {
-            font-size: 16px;
-        }
-    }
-    .upload-wrap {
-        margin: 12px 0 24px;
-    }
-    .example {
-        width: 80px;
-        height: 80px;
-    }
-    .example + .example {
-        margin-left: 12px;
-    }
-}
-
-
-.popup-role-text {
-    color: #2199f8;
-    background-color: rgba(33, 153, 248, 0.1);
-    padding: 0 6px;
-    font-size: 13px;
-    margin-left: 16px;
-    border-radius: 3px;
-    &.role-3 {
-        color: #7d7d7d;
-        background: rgba(243, 243, 243, 0.5);
-    }
-}
-
-.btn {
-    padding: 8px;
-    background: #2199f8;
-    border-radius: 25px;
-    color: #fff;
-    font-size: 16px;
-    text-align: center;
-    cursor: pointer;
-    transition: opacity 0.3s;
-    &.disabled {
-        opacity: 0.6;
-        cursor: not-allowed;
-        pointer-events: none;
-    }
-}
-
-.success-popup {
-    width: 300px;
-    border-radius: 14px;
-    padding: 28px 15px 20px;
-    box-sizing: border-box;
-    .success-wrap {
-        text-align: center;
-    }
-    .success-icon {
-        width: 68px;
-        height: 68px;
-    }
-    .success-title {
-        font-size: 24px;
-        margin-top: 12px;
-    }
-    .success-sub {
-        margin: 8px 0 32px;
-    }
-}
-</style>
-
-<style lang="scss">
-// 全局样式,用于设置下拉框的 z-index(不受 scoped 限制)
-.custom-select-dropdown {
-    z-index: 10000 !important;
-}
-</style>

+ 0 - 159
src/components/popup/executeTracePopup.vue

@@ -1,159 +0,0 @@
-<template>
-    <popup v-model:show="showValue" class="execute-trace-popup" closeable teleport="body" :z-index="9999">
-        <div class="trace-section">
-            <div class="section-title">执行轨迹</div>
-            <!-- 二维码横幅 -->
-            <div class="qr-banner">
-                <span class="banner-text">扫码执行,自动记录人工执行轨迹</span>
-                <img class="qr-code" src="@/assets/img/home/qrcode.png" alt="二维码" />
-            </div>
-            <!-- 图片上传区域 -->
-            <upload exampleImg>
-                <div class="upload-example">
-                    <img class="example" src="@/assets/img/home/example-4.png" alt="" />
-                    <img class="example" src="@/assets/img/home/plus.png" alt="" />
-                </div>
-            </upload>
-        </div>
-
-        <!-- 执行现场部分 -->
-        <div class="scene-section">
-            <div class="section-title">执行现场</div>
-            <!-- 二维码横幅 -->
-            <div class="qr-banner">
-                <span class="banner-text">邀请好友拍照,立即获取执行照片</span>
-                <img class="qr-code" src="@/assets/img/home/qrcode.png" alt="二维码" />
-            </div>
-            <!-- 图片上传区域 -->
-            <upload exampleImg>
-                <div class="upload-example">
-                    <img class="example" src="@/assets/img/home/example-4.png" alt="" />
-                    <img class="example" src="@/assets/img/home/plus.png" alt="" />
-                </div>
-            </upload>
-        </div>
-
-        <!-- 底部按钮 -->
-        <div class="popup-buttons">
-            <div class="btn-later" @click="handleLater">稍后上传</div>
-            <div class="btn-confirm" @click="handleConfirm">确认上传</div>
-        </div>
-    </popup>
-</template>
-
-<script setup>
-import { Popup } from "vant";
-import upload from "@/components/upload.vue";
-import { computed } from "vue";
-
-const props = defineProps({
-    // 控制弹窗显示/隐藏
-    show: {
-        type: Boolean,
-        default: false,
-    },
-});
-
-const emit = defineEmits(["update:show", "later", "confirm"]);
-
-// 处理v-model双向绑定
-const showValue = computed({
-    get: () => props.show,
-    set: (value) => emit("update:show", value),
-});
-
-// 稍后上传
-const handleLater = () => {
-    emit("later");
-    emit("update:show", false);
-};
-
-// 确认上传
-const handleConfirm = () => {
-    emit("confirm");
-    emit("update:show", false);
-};
-</script>
-
-<style scoped lang="scss">
-.execute-trace-popup {
-    width: 90%;
-    padding: 24px 16px 20px;
-    border-radius: 8px;
-    background: linear-gradient(360deg, #ffffff 74.2%, #d1ebff 100%);
-
-    ::v-deep {
-        .van-popup__close-icon {
-            color: #333333;
-        }
-    }
-
-    .trace-section,
-    .scene-section {
-        .section-title {
-            font-size: 16px;
-            margin-bottom: 12px;
-        }
-
-        .qr-banner {
-            display: flex;
-            align-items: center;
-            justify-content: space-between;
-            background: rgba(33, 153, 248, 0.1);
-            padding: 5px 8px;
-            border-radius: 5px;
-            margin-bottom: 12px;
-            border: 0.5px solid #2199f8;
-
-            .banner-text {
-                color: #2199f8;
-            }
-
-            .qr-code {
-                width: 32px;
-                height: 32px;
-            }
-        }
-        .upload-example {
-            display: flex;
-            justify-content: center;
-            align-items: center;
-            .example {
-                width: 80px;
-                height: 80px;
-            }
-            .example + .example {
-                margin-left: 12px;
-            }
-        }
-    }
-
-    .scene-section {
-        margin: 12px 0 24px 0;
-    }
-
-    .popup-buttons {
-        display: flex;
-        gap: 12px;
-
-        .btn-later,
-        .btn-confirm {
-            flex: 1;
-            padding: 6px;
-            border-radius: 2px;
-            text-align: center;
-        }
-
-        .btn-later {
-            color: #9D9D9D;
-            border: 1px solid #D7D7D7;
-        }
-
-        .btn-confirm {
-            background: #2199f8;
-            color: #ffffff;
-            border: 1px solid #2199f8;
-        }
-    }
-}
-</style>

+ 0 - 186
src/components/popup/uploadPopup.vue

@@ -1,186 +0,0 @@
-<template>
-    <popup class="upload-popup" v-model:show="show" closeable :close-on-click-overlay="false" @closed="handleClosed">
-        <div v-show="isPlan" class="plan-title">
-            请上传农事凭证
-        </div>
-        <div v-show="!isPlan">
-            <div class="title">
-                <img src="@/assets/img/home/msg_icon.png" alt="">
-                <span>如需激活农事,请上传照片</span>
-            </div>
-            <div class="tips">
-                {{text}}
-            </div>
-        </div>
-        <upload :textShow="true" :exampleImg="isPlan">
-            <img class="example" src="@/assets/img/home/example-3.png" alt="">
-        </upload>
-        <div class="footer">
-            <el-button type="primary" class="btn" @click="handleUpload">确认上传</el-button>
-        </div>
-    </popup>
-    <popup class="upload-popup loading" v-model:show="dialogVisible" closeable>
-      正在识别中,请稍后...
-    </popup>
-</template>
-
-<script setup>
-import { Popup } from 'vant';
-import {onActivated, onDeactivated, ref} from 'vue'
-import upload from '@/components/upload'
-import eventBus from "@/api/eventBus";
-import { ElMessage } from "element-plus";
-
-const props = defineProps({
-    executionData: {},
-});
-
-const show = ref(false);
-const gardenId = ref(null)
-const images = ref([])
-const dialogVisible = ref(false)
-
-onActivated(()=>{
-    eventBus.off('upload:changeArr',uploadChange)
-    eventBus.on('upload:changeArr',uploadChange)
-    eventBus.on('uploadUopup:show',handleShow)
-    eventBus.off('close:uploadPopup',closePupop)
-    eventBus.on('close:uploadPopup',closePupop)
-})
-
-function closePupop(){
-    dialogVisible.value = false
-}
-
-function uploadChange(arr){
-    images.value = arr
-}
-
-// 转换为 YYYY-MM-DD 格式
-function formatDate(date) {
-    let year = date.getFullYear();
-    let month = String(date.getMonth() + 1).padStart(2, '0'); // 月份是从0开始的
-    let day = String(date.getDate()).padStart(2, '0');
-    return `${year}-${month}-${day}`;
-}
-
-const handleUpload = () =>{
-    if(images.value.length === 0) return ElMessage.warning('请上传图片')
-    if(isPlan.value){
-        const params = {
-            ...props.executionData,
-            orderStatus: 5,
-            executeEvidence: images.value
-        }
-        VE_API.order.confirm(params).then(({ code }) => {
-            if (code === 0) {
-                show.value = false
-                ElMessage.success('您已上传成功')
-            }
-        })
-    }else{
-        const params = {
-            gardenId:gardenId.value,
-            images:images.value,
-            uploadDate:formatDate(new Date())
-        }
-        VE_API.ali.uploadImg(params).then(res =>{
-            if(res.success){
-                show.value = false
-                ElMessage.success('您已上传成功')
-                dialogVisible.value = true
-                eventBus.emit('confirm:callback')
-            }
-        })
-    }
-}
-
-const orderId = ref(null)
-const text = ref('')
-// 农事规划
-const isPlan = ref(false)
-
-function handleShow({isPlanVal,gardenIdVal,orderIdVal,textVal}){
-    orderId.value = orderIdVal
-    text.value = textVal
-    images.value = []
-    gardenId.value = gardenIdVal
-    show.value = true
-    isPlan.value = isPlanVal ? true : false
-}
-
-const handleClosed = () =>{
-    eventBus.emit('upload:reset')
-}
-
-onDeactivated(() => {
-    eventBus.off('uploadUopup:show',handleShow)
-    show.value = false
-})
-</script>
-
-<style lang="scss" scoped>
-.upload-popup{
-    width: 90%;
-    box-sizing: border-box;
-    padding: 20px 18px;
-    background: linear-gradient(0deg,#FFFFFF 70%,#D1EBFF 100%);
-    border-radius: 10px;
-    &.loading{
-        text-align: center;
-        padding: 40px 0;
-        font-size: 18px;
-    }
-    .example{
-        width: 100%;
-        height: 100%;
-    }
-    ::v-deep{
-        .van-popup__close-icon{
-            color: #000;
-        }
-    }
-    .plan-title {
-        text-align: center;
-        font-size: 26px;
-        font-family: "PangMenZhengDao";
-        padding: 12px 0 20px 0;
-    }
-    .title{
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        img{
-            width: 20px;
-            height: 20px;
-            margin-right: 8px;
-        }
-        span{
-            font-size: 18px;
-            font-family: "PangMenZhengDao";
-        }
-    }
-    .tips{
-        margin: 8px 0 16px 0;
-        border: 1px solid rgba(33, 153, 248, 0.52);
-        border-radius: 5px;
-        padding: 9px 5px;
-        font-family: "PangMenZhengDao";
-        font-size: 12px;
-        color: #2199F8;
-        text-align: center;
-        span{
-            color: #FF953D;
-        }
-    }
-    .footer{
-        margin-top: 24px;
-        display: flex;
-        justify-content: center;
-        .btn{
-            width: 80%;
-            padding: 18px;
-        }
-    }
-}
-</style>

+ 0 - 180
src/components/selectGroup.vue

@@ -1,180 +0,0 @@
-<template>
-    <div class="select-group">
-        <el-select class="custom-select" v-model="farmVal" size="large" v-show="!toggle">
-            <el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
-        </el-select>
-        <div class="weather-wrap" :class="{'weather-transition':toggle}">
-            <div class="weather-t">
-                <div class="weather-l">
-                    <div class="weather-icon">
-                        <i :class="'qi-' + weatherInfo.iconDay + '-fill'"></i>
-                    </div>
-                    <span>{{ weatherInfo.tempAvg }}°C</span>
-                </div>
-                <div class="weather-r">
-                    <div class="line"></div>
-                    {{ formattedDate }}
-                </div>
-                <div class="arrow" @click="handleToggle">
-                    <el-icon v-show="!toggle" size="12"><ArrowDownBold /></el-icon>
-                    <el-icon v-show="toggle" size="12"><ArrowUpBold /></el-icon>
-                </div>
-            </div>
-            <div class="weather-b">近14天晴天占比80%,温度波动较小,有利于荔枝糖分与风味积累</div>
-            <div v-if="toggle" class="weather-chart">
-                <span class="title">14天天气预报</span>
-                <div class="chart-wrap">
-                    <weather-chart class="chart-dom"></weather-chart>
-                </div>
-            </div>
-        </div>
-    </div>
-</template>
-
-<script setup>
-import { onMounted, ref } from "vue";
-import { convertPointToArray } from "@/utils/index";
-import weatherChart from "./weatherChart.vue"
-
-const toggle = ref(false)
-const handleToggle = () =>{
-    toggle.value = !toggle.value
-    emit('toggle',toggle.value)
-}
-
-const farmVal = ref("");
-const options = ref([]);
-
-const emit = defineEmits(['gardenList','toggle'])
-const getGardenList = () => {
-    VE_API.index.fetchGardenList({current: 1, size: 50}).then(res => {
-        options.value = res.data;
-        farmVal.value = res.data[0].id
-        emit('gardenList',res.data)
-    });
-};
-
-const weatherInfo = ref({});
-// 气象信息
-const fetchData = () => {
-    VE_API.home.weatherInfo({ point: localStorage.getItem('MINI_USER_LOCATION_POINT') }).then(({ data }) => {
-        data.tempAvg = (data.tempMin + data.tempMax) / 2;
-        weatherInfo.value = data || {};
-    });
-};
-
-const currentDate = new Date();
-
-const month = (currentDate.getMonth() + 1).toString().padStart(2, '0');
-const day = currentDate.getDate().toString().padStart(2, '0');
-
-// MM/DD
-const formattedDate = `${month}/${day}`;
-
-onMounted(() => {
-    fetchData();
-    getGardenList()
-});
-</script>
-
-<style lang="scss" scoped>
-.select-group {
-    width: 100%;
-    height: 100%;
-    display: flex;
-    justify-content: space-between;
-    align-items: baseline;
-    .custom-select{
-        width: 110px;
-        ::v-deep{
-            .el-select__wrapper{
-                box-shadow: none;
-                background: rgba(0, 0, 0, 0.3);
-                border: 1px solid rgba(255, 255, 255, 0.4);
-                border-radius: 20px;
-            }
-            .el-select__caret,.el-select__placeholder{
-                color: #fff;
-            }
-        }
-    }
-    .weather-wrap {
-        color: #fff;
-        background: rgba(0, 0, 0, 0.6);
-        border: 1px solid rgba(255, 255, 255, 0.4);
-        border-radius: 10px;
-        font-size: 12px;
-        width: calc(100% - 125px);
-        padding: 8px 10px;
-        box-sizing: border-box;
-        transition: all 0.3s;
-        backdrop-filter: blur(2px);
-        .weather-t{
-            display: flex;
-            align-items: center;
-            position: relative;
-            .arrow{
-                position: absolute;
-                right: 0;
-                top: 0;
-                width: 20px;
-                height: 20px;
-                text-align: center;
-                line-height: 22px;
-                border-radius: 50%;
-                background: rgba(255, 255, 255, 0.12);
-            }
-            .weather-l,
-            .weather-r {
-                display: flex;
-                align-items: center;
-            }
-            .weather-icon {
-                width: 18px;
-                height: 18px;
-                margin-right: 4px;
-                i {
-                    display: block;
-                    width: 18px;
-                    height: 18px;
-                    font-size: 18px;
-                    left: 1px;
-                    top: 2px;
-                }
-            }
-            .line {
-                margin: 0 6px 0 6px;
-                width: 1px;
-                height: 12px;
-                background: rgba(255, 255, 255, 0.3);
-            }
-        }
-        .weather-b{
-            margin-top: 6px;
-        }
-        &.weather-transition{
-            width: 100%;
-            .weather-b{
-                border-bottom: 1px solid rgba(255, 255, 255, 0.4);
-                padding-bottom: 10px;
-            }
-            .weather-chart{
-                margin-top: 12px;
-                .title{
-                    color: #bbbbbb;
-                    font-size: 11px;
-                }
-                .chart-wrap{
-                    width: 100%;
-                    height: 150px;
-                    overflow-x: auto;
-                    .chart-dom{
-                        width: 120%;
-                        height: 100%;
-                    }
-                }
-            }
-        }
-    }
-}
-</style>

+ 0 - 381
src/views/old_mini/monitor/subPages/farmInfo copy.vue

@@ -1,381 +0,0 @@
-<template>
-    <custom-header name="农场信息" bgColor="#f2f3f5"></custom-header>
-    <div class="farm-details-page">
-        <div class="map-wrap info-card">
-            <div class="map-area" ref="mapContainer"></div>
-            <div class="map-text" @click="handleEditMap">点击编辑地块</div>
-        </div>
-        <div class="info-box info-card">
-            <div class="section-header">
-                <div class="line-title">基本信息</div>
-                <div class="edit-btn-box">
-                    <div class="edit-btn" @click="handleAddVariety">新增品种</div>
-                    <div class="edit-btn" @click="handleEditFarmInfo">编辑信息</div>
-                </div>
-            </div>
-            <div class="info-list">
-                <div class="info-row">
-                    <span class="info-label">农场名称:</span>
-                    <span class="info-value">{{ farmInfo.subjectName }}</span>
-                </div>
-                <div class="info-row">
-                    <span class="info-label">联系人:</span>
-                    <span class="info-value">{{ farmInfo.contactName }}</span>
-                </div>
-                <div class="info-row center-row">
-                    <span class="info-label">联系电话:</span>
-                    <span class="info-value">{{ farmInfo.contactPhone }}</span>
-                </div>
-                <div class="info-row">
-                    <span class="info-label">种植面积:</span>
-                    <span class="info-value">{{ farmInfo.farmArea }}亩</span>
-                </div>
-                <div class="info-row">
-                    <span class="info-label">农场位置:</span>
-                    <span class="info-value">{{ farmInfo.farmAddress }}</span>
-                </div>
-                <div class="info-row">
-                    <span class="info-label">种植品种:</span>
-                    <div class="info-value crop-tags">
-                        <span v-for="crop in farmInfo.regionList" :key="crop.regionId" class="crop-tag">
-                            {{ crop.regionName }}
-                        </span>
-                    </div>
-                </div>
-            </div>
-        </div>
-        <div class="info-box info-card">
-            <div class="section-header">
-                <div class="line-title">农场设施</div>
-                <div class="edit-btn" @click="handleEditFarmFacility">编辑信息</div>
-            </div>
-            <div class="info-list">
-                <div class="info-row">
-                    <span class="info-label">灌溉方式:</span>
-                    <div class="info-value crop-tags">
-                        <span v-for="method in basicFarmInfo.irrigation" :key="method.code" class="crop-tag">
-                            {{ method.name }}
-                        </span>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-    <FarmInfoPopup ref="farmInfoPopupRef" :farmId="route.query.subjectId" @success="fetchFarmSubjectDetail" />
-</template>
-
-<script setup>
-import { ref, onMounted, nextTick } from "vue";
-import customHeader from "@/components/customHeader.vue";
-import { useRouter, useRoute } from "vue-router";
-import DrawRegionMap from "@/views/old_mini/interactionList/map/drawRegionMap.js";
-import * as util from "@/common/ol_common.js";
-import { ElMessage } from "element-plus";
-import { useStore } from "vuex";
-import FarmInfoPopup from "@/views/old_mini/home/components/farmInfoPopup.vue";
-
-const router = useRouter();
-const route = useRoute();
-const store = useStore();
-const farmInfo = ref({});
-const mapContainer = ref(null);
-const drawRegionMap = new DrawRegionMap();
-
-const initMap = () => {
-    if (!mapContainer.value) return;
-    if (drawRegionMap.kmap) return;
-
-    const info = farmInfo.value || {};
-
-    drawRegionMap.initMap(info.farmLocation, mapContainer.value, false, true, false);
-
-    // 回显农场区域,多边形 WKT 数组(有就画,没有就不画)
-    let geometryArr = [];
-
-    // 优先使用 regionList 里的 geom:有值才渲染
-    if (Array.isArray(info.regionList) && info.regionList.length > 0) {
-        info.regionList.forEach((item) => {
-            if (item?.geom) {
-                geometryArr.push(item?.geom)
-            }
-        });
-    }
-
-    if (geometryArr.length) {
-        // 农场信息页:地块用绿色展示;第三参留空表示面积仍按地块自动计算
-        drawRegionMap.setAreaGeometry(Array.from(geometryArr), true, undefined, {
-            fill: "rgba(0, 57, 44, 0.5)",
-            stroke: "#18AA8B",
-        });
-    }
-
-    // 在区域中心落一个点位(使用与勾画页相同的图标)
-    try {
-        const geom = util.wktCastGeom(info.farmLocation);
-        if (geom && typeof geom.getFirstCoordinate === "function") {
-            const coord = geom.getFirstCoordinate();
-            drawRegionMap.setMapPoint(coord);
-        }
-    } catch (e) {
-        // 解析失败则忽略点位
-    }
-};
-
-const destroyMap = () => {
-    if (drawRegionMap && drawRegionMap.kmap && drawRegionMap.destroyMap) {
-        drawRegionMap.destroyMap();
-    }
-};
-
-onMounted(() => {
-    fetchFarmSubjectDetail();
-    fetchBasicFarmFormData();
-});
-
-function fetchFarmSubjectDetail() {
-    VE_API.basic_farm.fetchFarmSubjectDetail({ subjectId: route.query.subjectId }).then(({ data }) => {
-        farmInfo.value = data || {};
-        nextTick(() => {
-            destroyMap();
-            initMap();
-        });
-    });
-}
-
-const basicFarmInfo = ref({});
-function fetchBasicFarmFormData() {
-    VE_API.basic_farm.fetchBasicFarmFormData({ subjectId: route.query.subjectId }).then(({ data, code }) => {
-        if (code === 0) {
-            basicFarmInfo.value = {
-                ...data,
-                irrigation: data.irrigationMethods.filter(item => item.selected),
-            }
-        }
-    });
-}
-
-const farmInfoPopupRef = ref(null);
-const handleEditFarmInfo = () => {
-    // // 回显地块:存到 polygonData,创建页会优先使用这里的数据
-    // if (data.geomWkt) {
-    //     const polygonData = {
-    //         geometryArr: [data.geomWkt],
-    //         mianji: data.mianji,
-    //         isConfirmed: true,
-    //     };
-    //     store.commit("home/SET_FARM_POLYGON", polygonData);
-    // } else {
-    //     store.commit("home/SET_FARM_POLYGON", null);
-    // }
-
-    // const params = {
-    //     ...farmInfo.value,
-    //     name: farmInfo.value.subjectName,
-    //     fzr: farmInfo.value.contactName,
-    //     tel: farmInfo.value.contactPhone,
-    //     mianji: farmInfo.value.farmArea,
-    //     address: farmInfo.value.farmAddress,
-    // };
-
-    // // 回显其他表单字段
-    // store.commit("home/SET_EDIT_FARM_DATA", params);
-
-    // // 带上 from=details,创建页提交/取消后能正确返回
-    // router.push(`/create_farm?type=edit&farmId=${route.query.subjectId}&from=details`);
-
-    farmInfoPopupRef.value.handleShow();
-};
-
-const handleEditFarmFacility = () => {
-    router.push(`/prescription?subjectId=${route.query.subjectId}`);
-};
-
-// 点击编辑地块
-const handleEditMap = () => {
-    const mapCenter = farmInfo.value.farmLocation || undefined;
-    if (farmInfo.value.regionList.length) {
-        // type=view 进入查看态;rangeWkt 用于回显多个地块
-        router.push({
-            path: "/draw_area",
-            query: {
-                type: "viewOnly",
-                subjectId: route.query.subjectId
-            },
-        });
-    } else {
-        ElMessage.warning("暂无种植作物,无法编辑地块");
-    }
-};
-
-const handleAddVariety = () => {
-    router.push(`/interaction?addVariety=true&subjectId=${route.query.subjectId}`);
-};
-</script>
-
-<style lang="scss" scoped>
-.farm-details-page {
-    background: #f2f3f5;
-    height: calc(100vh - 40px);
-    padding: 0 12px;
-    overflow: auto;
-
-    .line-title {
-        position: relative;
-        padding-left: 14px;
-        font-size: 16px;
-
-        &::before {
-            content: "";
-            position: absolute;
-            left: 5px;
-            top: 50%;
-            transform: translateY(-50%);
-            width: 4px;
-            height: 15px;
-            background: #2199f8;
-            border-radius: 20px;
-        }
-    }
-
-    .info-card{
-        margin-top: 12px;
-        background: #fff;
-        border-radius: 6px;
-        padding: 10px;
-    }
-
-    .map-wrap {
-        position: relative;
-
-        .map-area {
-            width: 100%;
-            height: 142px;
-            clip-path: inset(0px round 5px);
-        }
-
-        .map-text {
-            position: absolute;
-            right: 13px;
-            bottom: 16px;
-            font-size: 12px;
-            color: #ffffff;
-            background: rgba(0, 0, 0, 0.5);
-            padding: 8px 12px;
-            border-radius: 25px;
-            border: 1px solid rgba(255, 255, 255, 0.5);
-        }
-    }
-
-    .info-box {
-        margin-top: 12px;
-
-        .section-header {
-            display: flex;
-            align-items: center;
-            justify-content: space-between;
-        }
-
-        .edit-btn-box {
-            display: flex;
-            align-items: center;
-            gap: 8px;
-        }
-
-        .edit-btn {
-            padding: 4px 12px;
-            font-size: 12px;
-            color: #86909c;
-            border-radius: 999px;
-            border: 0.5px solid #d0d3d8;
-            background-color: #ffffff;
-        }
-
-        .info-list {
-            margin-top: 10px;
-            margin-left: 5px;
-
-            .info-row {
-                display: flex;
-                align-items: flex-start;
-                margin-bottom: 6px;
-
-                &.center-row {
-                    align-items: center;
-                }
-
-                .info-label {
-                    min-width: 80px;
-                    color: #4E5969;
-                }
-
-                .crop-tags {
-                    display: flex;
-                    align-items: center;
-                    flex-wrap: wrap;
-                    gap: 6px;
-
-                    .crop-tag {
-                        padding: 2px 8px;
-                        background: #E8F3FF;
-                        color: #2199f8;
-                        border-radius: 2px;
-                        font-size: 12px;
-                    }
-                }
-
-                .problem-tags {
-                    display: flex;
-                    flex-wrap: wrap;
-                    gap: 6px;
-
-                    .problem-tag {
-                        padding: 2px 8px;
-                        background: rgba(58, 173, 148, 0.1);
-                        color: #3AAD94;
-                        border-radius: 2px;
-                        font-size: 13px;
-                    }
-                }
-
-                .device-value {
-                    width: 100%;
-                }
-
-                .device-box {
-                    padding: 6px;
-                    border-radius: 4px;
-                    border: 0.5px solid rgba(33, 153, 248, 0.2);
-                    display: grid;
-                    grid-template-columns: repeat(2, minmax(0, 1fr));
-                    column-gap: 12px;
-                    row-gap: 4px;
-
-                    .device-item {
-                        display: flex;
-                        align-items: center;
-                        justify-content: space-between;
-                        padding: 4px 0;
-                        min-width: 0;
-                        gap: 8px;
-                        font-size: 13px;
-                        color: #1D2129;
-
-                        .device-count {
-                            padding: 2px 8px;
-                            background: #E8F3FF;
-                            color: #2199f8;
-                            border-radius: 2px;
-                        }
-                    }
-                }
-            }
-
-            .info-row-column {
-                display: flex;
-                flex-direction: column;
-                gap: 4px;
-            }
-        }
-    }
-}
-</style>

+ 0 - 728
src/views/old_mini/monitor/subPages/plan copy 2.vue

@@ -1,728 +0,0 @@
-<template>
-    <div class="plan-page">
-        <custom-header name="农事规划"></custom-header>
-        <div class="plan-content">
-            <div class="filter-wrap">
-                <div class="season-tabs">
-                    <div
-                        v-for="s in seasons"
-                        :key="s.value"
-                        class="season-tab"
-                        :class="{ active: s.value === activeSeason }"
-                        @click="handleSeasonClick(s.value)"
-                    >
-                        {{ s.label }}
-                    </div>
-                </div>
-                <div class="status-filter">
-                    <div v-for="status in statusList" :key="status.value" class="status-item" :class="status.color">
-                        <div class="status-dot"></div>
-                        <span class="status-text">{{ status.label }}</span>
-                    </div>
-                </div>
-            </div>
-            <div class="timeline-container" ref="timelineContainerRef">
-                <div class="timeline-list" :style="getListStyle">
-                    <div class="timeline-middle-line"></div>
-                    <!-- 物候期覆盖条(progress 为起点,progress2 为终点,单位 %) -->
-                    <div
-                        v-for="(p, idx) in phenologyList"
-                        :key="p.id ?? idx"
-                        class="phenology-bar"
-                        :style="getPhenologyBarStyle(p)"
-                    >
-                        <div class="reproductive-list">
-                            <div
-                                v-for="(r, rIdx) in Array.isArray(p.reproductiveList) ? p.reproductiveList : []"
-                                :key="r.id ?? rIdx"
-                                class="reproductive-item"
-                                :class="{
-                                    'horizontal-text': getReproductiveItemHeight(p) < 30,
-                                    'vertical-lr-text': getReproductiveItemHeight(p) >= 30
-                                }"
-                                :style="getReproductiveItemHeight(p) < 30 ? { '--item-height': `${getReproductiveItemHeight(p)}px` } : {}"
-                            >
-                                {{ r.name }}
-                                <div class="arranges" :class="{ 'no-wrap': getReproductiveItemHeight(p) <= 35 }">
-                                    <div
-                                        v-for="(fw, aIdx) in Array.isArray(r.farmWorkArrangeList)
-                                            ? r.farmWorkArrangeList
-                                            : []"
-                                        :key="fw.id ?? aIdx"
-                                        class="arrange-box"
-                                        :class="[
-                                            getArrangeStatusClass(fw),
-                                            {
-                                                'small-height': getReproductiveItemHeight(p) <= 35,
-                                                'two-chars': fw.farmWorkName && fw.farmWorkName.trim().length === 2,
-                                                'text-4-6': fw.farmWorkName && getTextLengthClass(fw.farmWorkName) === 'text-4-6',
-                                                'text-7-8': fw.farmWorkName && getTextLengthClass(fw.farmWorkName) === 'text-7-8'
-                                            }
-                                        ]"
-                                        @click="handleRowClick(fw)"
-                                    >
-                                        <span class="arrange-text">{{ formatTextWithLineBreak(fw.farmWorkName) }}</span>
-                                        <div
-                                            v-if="
-                                                getArrangeStatusClass(fw) === 'status-complete' ||
-                                                getArrangeStatusClass(fw) === 'status-warning'
-                                            "
-                                            class="status-icon"
-                                            :class="getArrangeStatusClass(fw)"
-                                        >
-                                            <el-icon
-                                                v-if="getArrangeStatusClass(fw) === 'status-complete'"
-                                                size="16"
-                                                color="#1CA900"
-                                            >
-                                                <SuccessFilled />
-                                            </el-icon>
-                                            <el-icon v-else size="18" color="#FF953D">
-                                                <WarnTriangleFilled />
-                                            </el-icon>
-                                        </div>
-                                    </div>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                    <div v-for="t in solarTerms" :key="t.id" class="timeline-term" :style="getTermStyle(t)">
-                        <span class="term-name">{{ t.displayName }}</span>
-                    </div>
-                </div>
-            </div>
-            <div class="control-section">
-                <div class="toggle-group">
-                    <el-switch v-model="isDefaultEnabled" />
-                    <span class="toggle-label">{{ isDefaultEnabled ? "默认" : "" }}发起农情需求</span>
-                </div>
-                <div class="add-button-group">
-                    <div class="add-button button" @click="addNewTask">新增农事</div>
-                    <div class="button" @click="manageTask">农事管理</div>
-                </div>
-            </div>
-        </div>
-    </div>
-    <!-- 农事信息弹窗 -->
-    <detail-dialog ref="detailDialogRef" @triggerFarmWork="triggerFarmWork"></detail-dialog>
-    <!-- 新增:激活上传弹窗 -->
-    <active-upload-popup @handleUploadSuccess="getFarmWorkPlan"></active-upload-popup>
-</template>
-
-<script setup>
-import { reactive, ref, onMounted, computed, nextTick } from "vue";
-import customHeader from "@/components/customHeader.vue";
-import { useRouter, useRoute } from "vue-router";
-import detailDialog from "@/components/detailDialog.vue";
-import eventBus from "@/api/eventBus";
-import activeUploadPopup from "@/components/popup/activeUploadPopup.vue";
-import { ElMessage } from "element-plus";
-import { SuccessFilled, WarningFilled } from "@element-plus/icons-vue";
-const router = useRouter();
-const route = useRoute();
-
-// 状态列表数据
-const seasons = reactive([
-    { value: "spring", label: "春季" },
-    { value: "summer", label: "夏季" },
-    { value: "autumn", label: "秋季" },
-    { value: "winter", label: "冬季" },
-]);
-const activeSeason = ref("");
-
-const statusList = reactive([
-    { value: "pending", label: "待触发", color: "gray" },
-    { value: "executing", label: "待完成", color: "blue" },
-    { value: "completed", label: "已完成", color: "green" },
-    { value: "expired", label: "已过期", color: "orange" },
-]);
-
-const solarTerms = ref([]);
-const phenologyList = ref([]);
-
-// 获取当前季节
-const getCurrentSeason = () => {
-    const month = new Date().getMonth() + 1; // 1-12
-    if (month >= 3 && month <= 5) {
-        return "spring"; // 春季:3-5月
-    } else if (month >= 6 && month <= 8) {
-        return "summer"; // 夏季:6-8月
-    } else if (month >= 9 && month <= 10) {
-        return "autumn"; // 秋季:9-10月
-    } else {
-        return "winter"; // 冬季:12-2月
-    }
-};
-
-onMounted(() => {
-    getFarmWorkPlan();
-});
-
-const getFarmWorkPlan = () => {
-    // 如果不是首次加载,保存当前滚动位置
-    let savedScrollTop = 0;
-    if (!isInitialLoad.value && timelineContainerRef.value) {
-        savedScrollTop = timelineContainerRef.value.scrollTop || 0;
-    }
-    
-    VE_API.monitor
-        .farmWorkPlan({ farmId: route.query.farmId })
-        .then(({ data, code }) => {
-            if (code === 0) {
-                const list = Array.isArray(data?.solarTermsList) ? data.solarTermsList : [];
-                const filtered = list
-                    .filter((t) => t && t.type === 1)
-                    .map((t) => ({
-                        id:
-                            t.id ??
-                            t.solarTermsId ??
-                            t.termId ??
-                            `${t.name || t.solarTermsName || t.termName || "term"}-${t.createDate || ""}`,
-                        displayName: t.name || t.solarTermsName || t.termName || "节气",
-                        createDate: t.createDate || null,
-                        progress: Number(t.progress) || 0,
-                    }));
-                solarTerms.value = filtered;
-                // 物候期数据
-                phenologyList.value = Array.isArray(data?.phenologyList)
-                    ? data.phenologyList.map((it) => ({
-                          id: it.id ?? it.phenologyId ?? it.name ?? `${it.progress}-${it.progress2}`,
-                          progress: Number(it.progress) || 0, // 起点 %
-                          progress2: Number(it.progress2) || 0, // 终点 %
-                          // 兼容多种可能的开始时间字段
-                          startTimeMs: safeParseDate(
-                              it.startDate || it.beginDate || it.startTime || it.start || it.start_at
-                          ),
-                          reproductiveList: Array.isArray(it.reproductiveList) ? it.reproductiveList : [],
-                      }))
-                    : [];
-                
-                // 等待 DOM 更新后处理滚动
-                nextTick(() => {
-                    if (isInitialLoad.value) {
-                        // 首次加载:设置默认季节为当前季节,并自动滚动到对应位置
-                        const currentSeason = getCurrentSeason();
-                        handleSeasonClick(currentSeason);
-                        isInitialLoad.value = false;
-                    } else {
-                        // 非首次加载:恢复之前的滚动位置
-                        if (timelineContainerRef.value && savedScrollTop > 0) {
-                            timelineContainerRef.value.scrollTop = savedScrollTop;
-                        }
-                    }
-                });
-            }
-        })
-        .catch((error) => {
-            console.error("获取农事规划数据失败:", error);
-        });
-};
-
-// 切换开关状态
-const isDefaultEnabled = ref(true);
-// 新增农事
-const addNewTask = () => {
-    ElMessage.warning("该功能正在升级中,敬请期待");
-    // router.push({
-    //     path: "/modify_work",
-    //     query: { data: JSON.stringify(["生长异常"]), gardenId: 766, isAdd: true },
-    // });
-};
-
-const triggerFarmWork = () => {
-    eventBus.emit("activeUpload:show", {
-        gardenIdVal: route.query.farmId,
-        problemTitleVal: '请选择您出现' + curFarmObj.value.farmWorkName + '的时间',
-        arrangeIdVal: curFarmObj.value.id,
-    });
-};
-
-const curFarmObj = ref({});
-const handleRowClick = (item) => {
-    curFarmObj.value = item;
-    // 0:默认,1-4:正常,5:完成,6:预警
-    if (item.flowStatus === 5) {
-        router.push({
-            path: "/review_work",
-            query: {
-                miniJson: JSON.stringify({ id: item.farmWorkRecordId,goBack: true }),
-            },
-        });
-    } else if (item.flowStatus === null) {
-        detailDialogRef.value.showDialog(item.farmWorkId);
-    } else if (item.flowStatus === 6 || (item.flowStatus < 5 && item.flowStatus >= 0)) {
-        router.push({
-            path: "/completed_work",
-            query: {
-                miniJson: JSON.stringify({ id: item.farmWorkRecordId }),
-            },
-        });
-    }
-};
-
-const manageTask = () => {
-    router.push({
-        path: "/agri_services_manage",
-        query: {
-            type: "manage",
-        },
-    });
-};
-
-const detailDialogRef = ref(null);
-const timelineContainerRef = ref(null);
-// 标记是否为首次加载
-const isInitialLoad = ref(true);
-
-// 安全解析时间到时间戳(ms)
-const safeParseDate = (val) => {
-    if (!val) return NaN;
-    if (val instanceof Date) return val.getTime();
-    if (typeof val === "number") return val;
-    if (typeof val === "string") {
-        // 兼容 "YYYY-MM-DD HH:mm:ss" -> Safari
-        const s = val.replace(/-/g, "/").replace("T", " ");
-        const d = new Date(s);
-        return isNaN(d.getTime()) ? NaN : d.getTime();
-    }
-    return NaN;
-};
-
-// 计算节气列表容器高度与项位置
-const getListStyle = computed(() => {
-    const total = (solarTerms.value?.length || 0) * 100;
-    const minH = 50 + total + 50; // 上下各 50
-    return { minHeight: `${minH}px` };
-});
-
-const getTermStyle = (t) => {
-    const p = Math.max(0, Math.min(100, Number(t?.progress) || 0));
-    const total = (solarTerms.value?.length || 0) * 100;
-    const top = 50 + (p / 100) * total;
-    return {
-        position: "absolute",
-        top: `${top}px`,
-        left: 0,
-        transform: "translateY(-50%)",
-        width: "30px",
-        height: "20px",
-        display: "flex",
-        alignItems: "center",
-    };
-};
-
-// 点击季节 → 滚动到对应节气(立春/立夏/立秋/立冬)
-const handleSeasonClick = (seasonValue) => {
-    activeSeason.value = seasonValue;
-    const mapping = {
-        spring: "立春",
-        summer: "立夏",
-        autumn: "立秋",
-        winter: "立冬",
-    };
-    const targetName = mapping[seasonValue];
-    if (!targetName) return;
-    const target = (solarTerms.value || []).find((t) => (t?.displayName || "") === targetName);
-    if (!target) return;
-    const p = Math.max(0, Math.min(100, Number(target.progress) || 0));
-    const total = (solarTerms.value?.length || 0) * 100;
-    const targetTop = 50 + (p / 100) * total; // 内容内的像素位置
-    const wrap = timelineContainerRef.value;
-    if (!wrap) return;
-    const viewH = wrap.clientHeight || 0;
-    const maxScroll = Math.max(0, wrap.scrollHeight - viewH);
-    // 将目标位置稍微靠上(使用 0.35 视口高度做偏移)
-    let scrollTop = Math.max(0, targetTop - viewH * 0.1);
-    if (scrollTop > maxScroll) scrollTop = maxScroll;
-    wrap.scrollTo({ top: scrollTop, behavior: "smooth" });
-};
-
-// 物候期覆盖条样式(使用像素计算,避免 100% 高度为 0 的问题)
-const getPhenologyBarStyle = (item) => {
-    const p1 = Math.max(0, Math.min(100, Number(item?.progress) || 0));
-    const p2 = Math.max(0, Math.min(100, Number(item?.progress2) || 0));
-    const start = Math.min(p1, p2);
-    const end = Math.max(p1, p2);
-    const total = (solarTerms.value?.length || 0) * 100; // 有效绘制区高度(px)
-    const topPx = 50 + (start / 100) * total;
-    const heightPx = Math.max(2, ((end - start) / 100) * total);
-    const now = Date.now();
-    const isFuture = Number.isFinite(item?.startTimeMs) ? item.startTimeMs > now : start > 0; // 无开始时间时按起点>0近似
-    // 反转:大于当前时间用灰色,小于等于当前时间用蓝色
-    const barColor = isFuture ? "rgba(145, 145, 145, 0.1)" : "#2199F8";
-    const beforeBg = isFuture ? "rgba(145, 145, 145, 0.1)" : "rgba(33, 153, 248, 0.1)";
-    return {
-        position: "absolute",
-        left: "46px",
-        width: "25px",
-        top: `${topPx}px`,
-        height: `${heightPx}px`,
-        background: barColor,
-        color: isFuture ? "#747778" : "#fff",
-        "--bar-before-bg": beforeBg,
-        zIndex: 2,
-    };
-};
-
-// 农事状态样式映射(0:默认,1-4:正常,5:完成,6:预警)
-const getArrangeStatusClass = (fw) => {
-    const t = fw?.flowStatus;
-    if (t == null) return "status-default";
-    if (t >= 0 && t <= 4) return "status-normal";
-    if (t === 5) return "status-complete";
-    if (t === 6) return "status-warning";
-    return "status-default";
-};
-
-// 计算 phenology-bar 的高度(px)
-const getPhenologyBarHeight = (item) => {
-    const p1 = Math.max(0, Math.min(100, Number(item?.progress) || 0));
-    const p2 = Math.max(0, Math.min(100, Number(item?.progress2) || 0));
-    const start = Math.min(p1, p2);
-    const end = Math.max(p1, p2);
-    const total = (solarTerms.value?.length || 0) * 100;
-    const heightPx = Math.max(2, ((end - start) / 100) * total);
-    return heightPx;
-};
-
-// 计算 reproductive-item 的高度(px)
-// 由于 reproductive-list 使用 grid-auto-rows: 1fr,每个 item 会等分 phenology-bar 的高度
-const getReproductiveItemHeight = (phenologyItem) => {
-    const barHeight = getPhenologyBarHeight(phenologyItem);
-    const listLength = Array.isArray(phenologyItem?.reproductiveList) ? phenologyItem.reproductiveList.length : 1;
-    // 如果列表为空,返回 barHeight;否则等分
-    return listLength > 0 ? barHeight / listLength : barHeight;
-};
-
-// 根据文字长度返回对应的 class
-const getTextLengthClass = (text) => {
-    if (!text || typeof text !== "string") return "";
-    const len = text.trim().length;
-    if (len > 4 && len <= 6) return "text-4-6";
-    if (len > 6 && len <= 8) return "text-7-8";
-    return "";
-};
-
-// 处理文本,在括号前换行
-const formatTextWithLineBreak = (text) => {
-    if (!text || typeof text !== "string") return text;
-    // 在左括号前添加换行符
-    return text.replace(/([((])/g, "\n$1");
-};
-
-</script>
-
-<style scoped lang="scss">
-.plan-page {
-    width: 100%;
-    height: 100vh;
-    background: #fff;
-    .plan-content {
-        .filter-wrap {
-            background: #fff;
-            padding: 13px 12px;
-            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
-            border-radius: 0 0 20px 20px;
-            .status-filter {
-                background: #fff;
-                padding: 3px 17px;
-                display: flex;
-                align-items: center;
-                gap: 16px;
-                font-size: 12px;
-
-                .status-item {
-                    display: flex;
-                    align-items: center;
-                    justify-content: center;
-                    gap: 6px;
-                    flex: 1;
-                    &.gray {
-                        color: #c4c6c9;
-                        .status-dot {
-                            background-color: #c4c6c9;
-                        }
-                    }
-
-                    &.blue {
-                        color: #2199f8;
-                        .status-dot {
-                            background-color: #2199f8;
-                        }
-                    }
-
-                    &.green {
-                        color: #1ca900;
-                        .status-dot {
-                            background-color: #1ca900;
-                        }
-                    }
-
-                    &.orange {
-                        color: #ff953d;
-                        .status-dot {
-                            background-color: #ff953d;
-                        }
-                    }
-                    .status-dot {
-                        width: 6px;
-                        height: 6px;
-                        border-radius: 50%;
-                    }
-                }
-            }
-
-            .season-tabs {
-                display: flex;
-                gap: 8px;
-                margin-bottom: 12px;
-                .season-tab {
-                    flex: 1;
-                    padding: 7px;
-                    text-align: center;
-                    background: #f3f3f3;
-                    color: #898a8a;
-                    border-radius: 3px;
-                    border: 1px solid transparent;
-                    font-size: 12px;
-                }
-                .season-tab.active {
-                    background: #ffffff;
-                    color: #2199f8;
-                    border-color: #2199f8;
-                }
-            }
-        }
-        .timeline-container {
-            height: calc(100vh - 93px - 40px - 73px);
-            overflow: auto;
-            position: relative;
-            box-sizing: border-box;
-            padding: 0 12px;
-            .timeline-list {
-                position: relative;
-            }
-            .timeline-middle-line {
-                position: absolute;
-                left: 15px; /* 位于节气文字列中间(列宽约30px) */
-                top: 50px;
-                bottom: 50px;
-                width: 2px;
-                background: #e8e8e8;
-                z-index: 1;
-            }
-            .phenology-bar {
-                display: flex;
-                align-items: stretch;
-                justify-content: center;
-                box-sizing: border-box;
-                &::before {
-                    content: "";
-                    position: absolute;
-                    top: 0;
-                    left: 25px;
-                    width: calc(100vw - 100px);
-                    height: 100%;
-                    background: var(--bar-before-bg, rgba(201, 201, 201, 0.1));
-                    z-index: 1;
-                }
-                .reproductive-list {
-                    display: grid;
-                    grid-auto-rows: 1fr; /* 子项等高,整体等分父高度 */
-                    align-items: stretch;
-                    justify-items: center; /* 子项居中 */
-                    width: 100%;
-                    height: 100%;
-                    box-sizing: border-box;
-                }
-                .reproductive-item {
-                    font-size: 12px;
-                    text-align: center;
-                    word-break: break-all;
-                    writing-mode: vertical-rl;
-                    text-orientation: upright;
-                    letter-spacing: 3px;
-                    width: 100%;
-                    line-height: 23px;
-                    color: inherit;
-                    position: relative;
-                    &.horizontal-text {
-                        writing-mode: horizontal-tb;
-                        text-orientation: mixed;
-                        letter-spacing: normal;
-                        line-height: calc(var(--item-height, 15px) - 3px);
-                    }
-                    &.vertical-lr-text {
-                        writing-mode: vertical-lr;
-                        text-orientation: upright;
-                        letter-spacing: 3px;
-                        line-height: 26px;
-                    }
-                    .arranges {
-                        position: absolute;
-                        left: 48px; /* 列与中线右侧一段距离 */
-                        top: 50%;
-                        transform: translateY(-50%);
-                        z-index: 3;
-                        display: flex;
-                        flex-wrap: wrap;
-                        flex-direction: row;
-                        align-items: center;
-                        max-width: calc(100vw - 100px);
-                        gap: 16px;
-                        &.no-wrap {
-                            flex-wrap: nowrap;
-                        }
-                        .arrange-box {
-                            width: 36px;
-                            height: 36px;
-                            border: 1px solid rgba(199, 199, 199, 0.6);
-                            border-radius: 2px;
-                            background: #fff;
-                            color: #a5a7a9;
-                            display: flex;
-                            align-items: center;
-                            justify-content: center;
-                            box-sizing: border-box;
-                            position: relative;
-                            font-size: 12px;
-                            flex-shrink: 0;
-                            &.small-height {
-                                height: 20px;
-                                width: 70px;
-                                &.two-chars {
-                                    width: 35px;
-                                }
-                                &.text-4-6 {
-                                    width: 65px;
-                                }
-                                &.text-7-8 {
-                                    width: 66px;
-                                }
-                                .arrange-text {
-                                    writing-mode: vertical-lr;
-                                    white-space: pre-line;
-                                }
-                            }
-                            &.text-4-6 {
-                                width: 65px;
-                            }
-                            &.text-7-8 {
-                                width: 66px;
-                            }
-                            .arrange-text {
-                                writing-mode: horizontal-tb;
-                                line-height: 14px;
-                                text-align: center;
-                                padding-left: 3px;
-                                white-space: pre-line;
-                            }
-                            .status-icon {
-                                position: absolute;
-                                right: -10px;
-                                bottom: -10px;
-                                z-index: 3;
-                            }
-                            &::before {
-                                content: "";
-                                position: absolute;
-                                left: -4px;
-                                top: 50%;
-                                transform: translateY(-50%);
-                                width: 0;
-                                height: 0;
-                                border-top: 4px solid transparent;
-                                border-bottom: 4px solid transparent;
-                                border-right: 4px solid currentColor; /* 与文字/边框颜色一致 */
-                            }
-                        }
-                        .arrange-box.status-warning {
-                            border-color: #ff953d;
-                            color: #ff953d;
-                        }
-                        .arrange-box.status-complete {
-                            border-color: #1ca900;
-                            color: #1ca900;
-                        }
-                        .arrange-box.status-normal {
-                            border-color: #2199f8;
-                            color: #2199f8;
-                        }
-                    }
-                }
-            }
-            .reproductive-item + .reproductive-item {
-                border-top: 2px solid #fff;
-            }
-            .phenology-bar + .phenology-bar {
-                border-top: 2px solid #fff;
-            }
-            .timeline-term {
-                position: absolute;
-                width: 30px;
-                padding-right: 16px;
-                display: flex;
-                align-items: center;
-                z-index: 2; /* 置于中线之上 */
-                .term-name {
-                    display: inline-block;
-                    width: 100%;
-                    height: 46px;
-                    line-height: 30px;
-                    background: #fff;
-                    font-size: 13px;
-                    word-break: break-all;
-                    writing-mode: vertical-rl;
-                    text-orientation: upright;
-                    color: rgba(174, 174, 174, 0.6);
-                    letter-spacing: 2px;
-                    text-align: center;
-                }
-            }
-        }
-
-        // 控制区域样式
-        .control-section {
-            position: fixed;
-            width: 100%;
-            left: 0;
-            box-sizing: border-box;
-            bottom: 0px;
-            background: #fff;
-            padding: 16px 12px;
-            display: flex;
-            justify-content: space-between;
-            align-items: center;
-            border-top: 1px solid #f0f0f0;
-
-            .toggle-group {
-                display: flex;
-                align-items: center;
-                gap: 8px;
-
-                .toggle-label {
-                    font-size: 13px;
-                    color: #141414;
-                }
-            }
-
-            .add-button-group {
-                display: flex;
-                align-items: center;
-                gap: 8px;
-                .button {
-                    color: #2199f8;
-                    border-radius: 25px;
-                    padding: 9px 15px;
-                    border: 1px solid #2199f8;
-                }
-                .add-button {
-                    background: linear-gradient(120deg, #76c3ff 0%, #2199f8 100%);
-                    color: white;
-                    border: 1px solid transparent;
-                }
-            }
-        }
-    }
-}
-</style>

+ 0 - 1006
src/views/old_mini/monitor/subPages/plan copy.vue

@@ -1,1006 +0,0 @@
-<template>
-    <div class="plan-page">
-        <custom-header name="农事规划"></custom-header>
-        <div class="plan-content">
-            <div class="filter-wrap">
-                <div class="season-tabs">
-                    <div
-                        v-for="s in seasons"
-                        :key="s.value"
-                        class="season-tab"
-                        :class="{ active: s.value === activeSeason }"
-                        @click="activeSeason = s.value"
-                    >
-                        {{ s.label }}
-                    </div>
-                </div>
-                <div class="status-filter">
-                    <div v-for="status in statusList" :key="status.value" class="status-item" :class="status.color">
-                        <div class="status-dot"></div>
-                        <span class="status-text">{{ status.label }}</span>
-                    </div>
-                </div>
-            </div>
-
-            <!-- 三行循环时间线 -->
-            <div class="cycle-timeline-container">
-                <div class="cycle-timeline">
-                    <div
-                        v-for="(row, rowIndex) in timelineRows"
-                        :key="rowIndex"
-                        class="cycle-row"
-                        :class="{ 'odd-index': rowIndex % 2 === 1 }"
-                    >
-                        <div
-                            v-for="(item, itemIndex) in row.items"
-                            :key="itemIndex"
-                            class="cycle-item"
-                            @click="handleRowClick(item)"
-                            :class="[item.type + '-item']"
-                        >
-                            <!-- 节气节点 -->
-                            <template v-if="item.type === 'term'">
-                                <!-- <div class="cycle-term-dot"></div> -->
-                                <div class="cycle-term-label">{{ item.name || item.id }}</div>
-                            </template>
-                        </div>
-
-                        <!-- 生育期名称(根据时间范围显示在对应行) -->
-                        <div class="cycle-phenology-wrap" v-if="getPhenologyBarsForRow(rowIndex).length > 0">
-                            <div
-                                v-for="p in getPhenologyBarsForRow(rowIndex)"
-                                :key="p.id"
-                                class="cycle-label"
-                                :class="p.color"
-                                :style="
-                                    isOddVisualRow(rowIndex)
-                                        ? { right: p.left, width: p.width }
-                                        : { left: p.left, width: p.width }
-                                "
-                            >
-                                {{ p.name }}
-                                <div v-if="p.arranges && p.arranges.length" class="arranges">
-                                    <div v-for="a in p.arranges" :key="a.id" :class="['cycle-task-box', a.status]">
-                                        <div class="cycle-task-text">{{ a.farmWorkName || a.name }}</div>
-                                        <!-- 任务连接器 -->
-                                        <div class="cycle-task-connector"></div>
-                                        <div
-                                            v-if="a.status === 'complete' || a.status === 'warning'"
-                                            class="status-icon"
-                                            :class="a.status"
-                                        >
-                                            <el-icon v-if="a.status === 'complete'" size="16" color="#1CA900"
-                                                ><SuccessFilled
-                                            /></el-icon>
-                                            <el-icon v-else size="18" color="#FF953D"><WarnTriangleFilled /></el-icon>
-                                        </div>
-                                    </div>
-                                </div>
-                            </div>
-                        </div>
-                        <!-- 行连接器 -->
-                        <div
-                            v-if="rowIndex < timelineRows.length - 1"
-                            class="cycle-connector"
-                            :class="[
-                                rowIndex % 2 === 1 ? 'middle-connector' : 'top-connector',
-                                getConnectorColorClass(rowIndex),
-                            ]"
-                        >
-                            <img v-if="isConnectorGray(rowIndex)" src="@/assets/img/monitor/defalut-arrow.png" alt="" />
-                            <img v-else src="@/assets/img/monitor/arrow.png" alt="" />
-                        </div>
-                    </div>
-                </div>
-            </div>
-
-            <div class="control-section">
-                <div class="toggle-group">
-                    <el-switch v-model="isDefaultEnabled" />
-                    <span class="toggle-label">{{ isDefaultEnabled ? "默认" : "" }}发起农情需求</span>
-                </div>
-                <div class="add-button-group">
-                    <div class="add-button button" @click="addNewTask">新增农事</div>
-                    <div class="button" @click="manageTask">农事管理</div>
-                </div>
-            </div>
-        </div>
-    </div>
-    <!-- 农事信息弹窗 -->
-    <detail-dialog ref="detailDialogRef"></detail-dialog>
-    <!-- 新增:激活上传弹窗 -->
-    <active-upload-popup></active-upload-popup>
-</template>
-
-<script setup>
-import { reactive, ref, onMounted, nextTick, onBeforeUnmount } from "vue";
-import customHeader from "@/components/customHeader.vue";
-import { useRouter, useRoute } from "vue-router";
-import detailDialog from "@/components/detailDialog.vue";
-import activeUploadPopup from "@/components/popup/activeUploadPopup.vue";
-const router = useRouter();
-const route = useRoute();
-
-// 状态列表数据
-const seasons = reactive([
-    { value: "spring", label: "春季" },
-    { value: "summer", label: "夏季" },
-    { value: "autumn", label: "秋季" },
-    { value: "winter", label: "冬季" },
-]);
-const activeSeason = ref("spring");
-
-const statusList = reactive([
-    { value: "pending", label: "待触发", color: "gray" },
-    { value: "executing", label: "待完成", color: "blue" },
-    { value: "completed", label: "已完成", color: "green" },
-    { value: "expired", label: "已过期", color: "orange" },
-]);
-
-// 切换开关状态
-const isDefaultEnabled = ref(true);
-
-// 时间线行数据(由接口节气生成)
-const timelineRows = reactive([]);
-
-// 目标定位日期(当前生育期参考点)
-const targetDate = new Date("2025-04-04T00:00:00");
-// 每行“当前生育期”标记的位置样式(按行索引)
-const phenologyPositions = ref({});
-
-// 生育期条(按行分组)
-const phenologyBarsByRow = ref([]);
-// 每一行可视区域的实际像素宽度(用于将最小像素宽度换算为百分比)
-const rowWidths = ref([]);
-// 节气 id 到对象的索引,便于通过 id 查找节气日期
-let solarTermIdToTerm = {};
-// 接口返回的生育期数据
-const phenologyList = ref([]);
-
-// 安全日期解析(兼容 'YYYY-MM-DD HH:mm:ss' / 'YYYY/MM/DD HH:mm:ss')
-const parseDate = (val) => {
-    if (!val) return null;
-    if (val instanceof Date) return isNaN(val.getTime()) ? null : val;
-    if (typeof val === "number") return new Date(val);
-    if (typeof val === "string") {
-        // 统一到可被 Safari 解析的格式
-        const s = val.replace(/-/g, "/").replace("T", " ");
-        const d = new Date(s);
-        return isNaN(d.getTime()) ? null : d;
-    }
-    return null;
-};
-
-onMounted(() => {
-    getFarmWorkPlan();
-    window.addEventListener("resize", handleResize, { passive: true });
-});
-onBeforeUnmount(() => {
-    window.removeEventListener("resize", handleResize);
-});
-
-const handleResize = () => {
-    // 重新测量并基于最新宽度重算条目
-    nextTick(() => {
-        measureRowWidths();
-        // 需要基于最新数据重算
-        if (phenologyList.value && phenologyList.value.length && cachedValidSolarTerms.value) {
-            groupPhenologyBarsByRow(phenologyList.value, cachedValidSolarTerms.value);
-        }
-    });
-};
-
-// 缓存已过滤/排序后的节气用于重复计算
-const cachedValidSolarTerms = ref(null);
-
-const getFarmWorkPlan = () => {
-    const paramFarmId = Number(route.query.farmId) || undefined;
-    VE_API.monitor
-        .farmWorkPlan({ farmId: paramFarmId ?? 92844 }) // 优先使用路由传入的 farmId
-        .then(({ data, code }) => {
-            if (code === 0) {
-                const solarTermsList = data.solarTermsList;
-                // 仅保留 type === 1 的节气,按需要的顺序(示例:反转)
-                // 取 type===1 的节气,并按日期降序排序(晚到早)
-                const validSolarTerms = Array.isArray(solarTermsList)
-                    ? solarTermsList
-                          .filter((t) => t && t.type === 1 && t.createDate)
-                          .sort((a, b) => {
-                              const da = parseDate(a.createDate)?.getTime() ?? 0;
-                              const db = parseDate(b.createDate)?.getTime() ?? 0;
-                              return db - da;
-                          })
-                    : [];
-                cachedValidSolarTerms.value = validSolarTerms;
-                generateTimelineData(validSolarTerms);
-                computeCurrentPhenologyPositions(validSolarTerms, targetDate);
-                // 保存生育期数据并生成各行生育期条
-                phenologyList.value = Array.isArray(data.phenologyList) ? data.phenologyList : [];
-                // 生成 id->term 的索引
-                solarTermIdToTerm = {};
-                validSolarTerms.forEach((t) => {
-                    if (t && (t.id || t.solarTermsId)) solarTermIdToTerm[t.id ?? t.solarTermsId] = t;
-                });
-                // 先等待 DOM 渲染完成后测量每行宽度,再据此计算最小可显示宽度
-                nextTick(() => {
-                    measureRowWidths();
-                    groupPhenologyBarsByRow(phenologyList.value, validSolarTerms);
-                });
-            }
-        })
-        .catch((error) => {
-            console.error("获取农事规划数据失败:", error);
-        });
-};
-
-// 测量每一行生育期容器的实际宽度
-const measureRowWidths = () => {
-    const rows = document.querySelectorAll(".cycle-timeline .cycle-row");
-    const widths = [];
-    rows.forEach((rowEl, idx) => {
-        const wrap = rowEl.querySelector(".cycle-phenology-wrap");
-        widths[idx] = wrap ? wrap.offsetWidth : 0;
-    });
-    rowWidths.value = widths;
-};
-
-// 生成时间轴数据
-const generateTimelineData = (solarTerms) => {
-    // 清空
-    timelineRows.splice(0, timelineRows.length);
-
-    // 无数据则给一行示例
-    if (!solarTerms || solarTerms.length === 0) {
-        timelineRows.push({
-            items: [
-                { type: "task", status: "default", taskName: "梢期", taskDesc: "杀虫" },
-                { type: "term", name: "节气" },
-                { type: "task", status: "default", taskName: "梢期", taskDesc: "杀虫" },
-                { type: "term", name: "节气" },
-                { type: "task", status: "default", taskName: "梢期", taskDesc: "杀虫" },
-                { type: "term", name: "节气" },
-            ],
-        });
-        return;
-    }
-
-    const itemsPerRow = 6; // 任务/节气交替
-    const termsPerRow = 3; // 每行3个节气
-    const totalRows = Math.ceil(solarTerms.length / termsPerRow);
-
-    for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) {
-        const rowItems = [];
-        const startTermIndex = rowIndex * termsPerRow;
-
-        for (let i = 0; i < itemsPerRow; i++) {
-            if (i % 2 === 0) {
-                // 任务位
-                const taskData = getTaskDataForIndex(Math.floor(i / 2));
-                rowItems.push({
-                    type: "task",
-                    status: taskData.status,
-                    taskName: taskData.taskName,
-                    taskDesc: taskData.taskDesc,
-                    icon: taskData.icon,
-                });
-            } else {
-                // 节气位
-                const termIndex = startTermIndex + Math.floor(i / 2);
-                if (termIndex < solarTerms.length) {
-                    const term = solarTerms[termIndex] || {};
-                    rowItems.push({
-                        type: "term",
-                        status: "default",
-                        name: term.name || term.solarTermsName || term.termName || "节气",
-                        id: term.id,
-                        createDate: term.createDate,
-                    });
-                } else {
-                    // 不足时补任务
-                    rowItems.push({
-                        type: "task",
-                        status: "default",
-                        taskName: "梢期",
-                        taskDesc: "杀虫",
-                    });
-                }
-            }
-        }
-
-        timelineRows.push({ items: rowItems });
-    }
-};
-
-// 任务占位数据(可按需接后端)
-const getTaskDataForIndex = (index) => {
-    const defaultTasks = [
-        { status: "default", taskName: "梢期", taskDesc: "杀虫" },
-        { status: "active", taskName: "梢期", taskDesc: "营养" },
-        { status: "complete", taskName: "梢期", taskDesc: "修剪", icon: { type: "complete" } },
-        { status: "warning", taskName: "梢期", taskDesc: "施肥", icon: { type: "warning" } },
-        { status: "normal", taskName: "梢期", taskDesc: "灌溉", icon: { type: "normal" } },
-    ];
-    return defaultTasks[index % defaultTasks.length];
-};
-
-// 计算“当前生育期”在各行的定位(只在包含目标日期的那一行显示)
-const computeCurrentPhenologyPositions = (solarTerms, date) => {
-    phenologyPositions.value = {};
-    if (!Array.isArray(solarTerms) || solarTerms.length === 0 || !(date instanceof Date)) return;
-
-    const termsPerRow = 3;
-    const totalRows = Math.ceil(solarTerms.length / termsPerRow);
-
-    // 1) 找到最接近目标日期的节气(按时间升序)
-    const termsAsc = solarTerms
-        .filter((t) => t && t.createDate)
-        .slice()
-        .sort((a, b) => (parseDate(a.createDate)?.getTime() ?? 0) - (parseDate(b.createDate)?.getTime() ?? 0));
-    if (termsAsc.length === 0) return;
-
-    const targetMs = date.getTime();
-    let nearest = termsAsc[0];
-    let bestDiff = Math.abs((parseDate(nearest.createDate)?.getTime() ?? 0) - targetMs);
-    for (let i = 1; i < termsAsc.length; i++) {
-        const ms = parseDate(termsAsc[i].createDate)?.getTime() ?? 0;
-        const diff = Math.abs(ms - targetMs);
-        if (diff < bestDiff) {
-            bestDiff = diff;
-            nearest = termsAsc[i];
-        }
-    }
-
-    // 2) 将该节气映射回当前(降序)数组中的索引与行
-    const nearestIdxDesc = solarTerms.findIndex((t) => t && nearest && t.id === nearest.id);
-    const rowIndex = Math.max(0, Math.floor(nearestIdxDesc / termsPerRow));
-
-    const startIdx = rowIndex * termsPerRow;
-    const endIdx = Math.min(startIdx + termsPerRow - 1, solarTerms.length - 1);
-    if (startIdx > endIdx) return;
-
-    const rowTerms = solarTerms.slice(startIdx, endIdx + 1);
-    // 视觉顺序用于方向(偶数行正向,奇数行反向),但时间范围应取该行真实最早/最晚
-    const rowDates = rowTerms
-        .map((t) => parseDate(t?.createDate))
-        .filter((d) => d && !isNaN(d.getTime()))
-        .map((d) => d.getTime());
-    if (rowDates.length === 0) return;
-    const minMs = Math.min(...rowDates);
-    const maxMs = Math.max(...rowDates);
-    const rowStart = new Date(minMs);
-    const rowEnd = new Date(maxMs);
-
-    // 3) 若目标日期不在该行范围内,则就近夹到边界(避免跨行导致丢失)
-    let anchorMs = targetMs;
-    if (anchorMs < minMs) anchorMs = minMs;
-    if (anchorMs > maxMs) anchorMs = maxMs;
-
-    // 4) 计算在该行范围内的比例
-    const total = Math.max(1, maxMs - minMs);
-    const ratio = Math.max(0, Math.min(1, (anchorMs - minMs) / total));
-    const percent = `${(ratio * 100).toFixed(2)}%`;
-
-    // 5) 偶数行用 left,奇数行用 right,与 Z 字方向一致
-    if (rowIndex % 2 === 1) {
-        phenologyPositions.value[rowIndex] = { right: percent };
-    } else {
-        phenologyPositions.value[rowIndex] = { left: percent };
-    }
-};
-// moved above with other refs
-
-// 将生育期条按行计算定位与宽度
-const groupPhenologyBarsByRow = (phenologyList, solarTerms) => {
-    phenologyBarsByRow.value = [];
-    if (
-        !Array.isArray(phenologyList) ||
-        phenologyList.length === 0 ||
-        !Array.isArray(solarTerms) ||
-        solarTerms.length === 0
-    ) {
-        return;
-    }
-
-    const termsPerRow = 3;
-    const totalRows = Math.ceil(solarTerms.length / termsPerRow);
-
-    // 行范围:使用该行包含的节气最早/最晚时间(按真实时间线性映射)
-    const rowRanges = [];
-    for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) {
-        const startIdx = rowIndex * termsPerRow;
-        const endIdx = Math.min(startIdx + termsPerRow - 1, solarTerms.length - 1);
-        const rowTerms = solarTerms.slice(startIdx, endIdx + 1);
-        const rowDates = rowTerms
-            .map((t) => parseDate(t?.createDate))
-            .filter((d) => d && !isNaN(d.getTime()))
-            .map((d) => d.getTime());
-        if (rowDates.length === 0) continue;
-        const minMs = Math.min(...rowDates);
-        const maxMs = Math.max(...rowDates);
-        const rowStart = new Date(minMs);
-        const rowEnd = new Date(maxMs);
-        const totalMs = Math.max(1, rowEnd.getTime() - rowStart.getTime());
-        rowRanges.push({ rowIndex, rowStart, rowEnd, totalMs });
-        phenologyBarsByRow.value[rowIndex] = [];
-    }
-
-    // 中点归属法:每条生育期归属到中点所在的行;在行内按 Z 字方向计算 left/right 与 width
-    phenologyList.forEach((p, pIndex) => {
-        const list = Array.isArray(p?.reproductiveList) ? p.reproductiveList : [];
-        const baseColorClass = pIndex % 2 === 0 ? "blue" : "orange";
-        list.forEach((r) => {
-            // 优先使用节气 id 对应的节气日期
-            let sTermDate = null;
-            let eTermDate = null;
-            if (r?.startSolarTermId && solarTermIdToTerm[r.startSolarTermId]?.createDate) {
-                sTermDate = parseDate(solarTermIdToTerm[r.startSolarTermId].createDate);
-            }
-            if (r?.endSolarTermId && solarTermIdToTerm[r.endSolarTermId]?.createDate) {
-                eTermDate = parseDate(solarTermIdToTerm[r.endSolarTermId].createDate);
-            }
-
-            const s = sTermDate || parseDate(r?.startDate);
-            const e = eTermDate || parseDate(r?.endDate);
-            if (!s || !e) return;
-            const start = new Date(Math.min(s.getTime(), e.getTime()));
-            const end = new Date(Math.max(s.getTime(), e.getTime()));
-            if (end < start) return;
-            const mid = new Date(start.getTime() + (end.getTime() - start.getTime()) / 2);
-
-            // 找到中点所在行;若不在任何行,则归最近行
-            let target = rowRanges.find(({ rowStart, rowEnd }) => mid >= rowStart && mid <= rowEnd);
-            if (!target) {
-                target = rowRanges.reduce((best, curr) => {
-                    const dist =
-                        mid < curr.rowStart
-                            ? curr.rowStart.getTime() - mid.getTime()
-                            : mid.getTime() - curr.rowEnd.getTime();
-                    if (!best || dist < best.dist) return { dist, curr };
-                    return best;
-                }, null)?.curr;
-            }
-            if (!target) return;
-
-            // 位置:基于真实的 startDate(不截断),确保相邻条的间距 = (后一个startDate - 前一个endDate) 的时间差映射
-            const startRatio = (start.getTime() - target.rowStart.getTime()) / target.totalMs;
-
-            // 宽度:基于真实的 endDate - startDate 的时间差
-            const actualDuration = end.getTime() - start.getTime();
-            const widthRatio = actualDuration / target.totalMs;
-
-            // 限制到行范围内
-            const leftRatio = Math.max(0, Math.min(1, startRatio));
-            const rightRatio = Math.max(0, Math.min(1, (end.getTime() - target.rowStart.getTime()) / target.totalMs));
-            let clampedWidthRatio = Math.max(0.001, Math.min(widthRatio, rightRatio - leftRatio));
-
-            // 强制最小显示宽度:若换算到像素后小于 CSS 中的 min-width:22px,则使用最小可见宽度
-            const MIN_LABEL_PX = 22; // 与样式 .cycle-label 的最小宽度保持一致
-            const rowWidthPx = rowWidths.value?.[target.rowIndex] || 0;
-            let leftPercent = leftRatio * 100;
-            let widthPercent = clampedWidthRatio * 100;
-            if (rowWidthPx > 0) {
-                const minPercent = (MIN_LABEL_PX / rowWidthPx) * 100;
-                if (widthPercent < minPercent) {
-                    widthPercent = minPercent;
-                }
-                // 若越界则左移以保证完全可见
-                if (leftPercent + widthPercent > 100) {
-                    leftPercent = Math.max(0, 100 - widthPercent);
-                }
-                // 回填为比例供后续使用
-                clampedWidthRatio = widthPercent / 100;
-            } else {
-                // 无法测量时,保底给一个不至于 0 的最小显示比例(以 360px 近似,22px/360≈6.1%)
-                if (widthPercent < 6.2) {
-                    widthPercent = 6.2;
-                    if (leftPercent + widthPercent > 100) leftPercent = Math.max(0, 100 - widthPercent);
-                    clampedWidthRatio = widthPercent / 100;
-                }
-            }
-
-            const isFuture = start.getTime() > Date.now();
-            const colorToUse = isFuture ? "" : baseColorClass;
-            // 组装农事安排:按 reproductiveId 归属到当前生育期
-            const arrangeList = Array.isArray(r.farmWorkArrangeList)
-                ? r.farmWorkArrangeList.filter((fw) => !fw.reproductiveId || fw.reproductiveId === r.id)
-                : [];
-            const arrangeItems = arrangeList.map((fw) => {
-                let status = "default";
-                const t = fw.farmWorkType;
-                if (t == null || t === 0) {
-                    status = "default";
-                } else if (t >= 1 && t <= 4) {
-                    status = "normal";
-                } else if (t === 5) {
-                    status = "complete";
-                } else if (t === 6) {
-                    status = "warning";
-                }
-                return {
-                    id: fw.id,
-                    name: fw.farmWorkName,
-                    status,
-                };
-            });
-
-            // // 附加两条测试数据:已完成、已过期
-            // arrangeItems.push(
-            //     { id: `${r.id}-test-complete`, name: "测试完成", status: "complete" },
-            //     { id: `${r.id}-test-warning`, name: "测试过期", status: "warning" }
-            // );
-
-            phenologyBarsByRow.value[target.rowIndex].push({
-                id: r.id || `${p.id || "p"}-${start.getTime()}-${end.getTime()}`,
-                name: r.name && r.name.trim() ? r.name.trim() : r.phenologyName || "生育期",
-                left: `${leftPercent.toFixed(4)}%`,
-                width: `${(clampedWidthRatio * 100).toFixed(4)}%`,
-                startTime: start.getTime(), // 用于排序,确保相邻条的顺序正确
-                color: colorToUse,
-                arranges: arrangeItems,
-            });
-        });
-    });
-
-    // 每行内部按 startTime 排序,确保相邻条的间距正确反映时间差
-    phenologyBarsByRow.value.forEach((rowBars) => {
-        rowBars.sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
-    });
-};
-
-// 获取指定行的生育期条
-const getPhenologyBarsForRow = (rowIndex) => {
-    return phenologyBarsByRow.value[rowIndex] || [];
-};
-
-// 视觉奇偶:自下而上计算奇偶(与 UI Z 字一致)
-const isOddVisualRow = (rowIndex) => {
-    const total = timelineRows.length;
-    if (total <= 0) return rowIndex % 2 === 1;
-    const visualIndex = total - 1 - rowIndex;
-    return visualIndex % 2 === 1;
-};
-
-// 新增农事
-const addNewTask = () => {
-    router.push({
-        path: "/modify_work",
-        query: { data: JSON.stringify(["生长异常"]), gardenId: 766, isAdd: true },
-    });
-};
-
-const manageTask = () => {
-    router.push({
-        path: "/agri_services_manage",
-        query: {
-            type: "manage",
-        },
-    });
-};
-
-const detailDialogRef = ref(null);
-
-const handleRowClick = (item) => {
-    if (item.status === "complete") {
-        router.push({
-            path: "/review_work",
-            query: {
-                miniJson: JSON.stringify({ id: item.id,goBack: true })
-            },
-        });
-    } else if (item.type !== "term" && item.status === "default") {
-        detailDialogRef.value.showDialog();
-    } else if (item.status === "warning" || item.status === "normal") {
-        router.push({
-            path: "/completed_work",
-            query: {
-                miniJson: JSON.stringify({ id: item.id })
-            },
-        });
-        // router.push({
-        //     path: "/services_agri",
-        //     query: {
-        //         id: item.id,
-        //         status: item.status,
-        //     },
-        // });
-    }
-};
-
-// 行连接器颜色:若后续生育期未开始则灰色,否则保持其颜色(蓝/橙)
-const getConnectorColorClass = (rowIndex) => {
-    const nextIndex = rowIndex + 1;
-    const bars = getPhenologyBarsForRow(nextIndex);
-    if (!bars || bars.length === 0) return "";
-
-    const nextIsOddIndex = nextIndex % 2 === 1; // 奇数行为左侧连接器
-
-    const parsePercent = (val) => {
-        if (typeof val !== "string") return 0;
-        const n = parseFloat(val.replace("%", ""));
-        return isNaN(n) ? 0 : n;
-    };
-
-    let target = bars[0];
-    if (nextIsOddIndex) {
-        // 左侧:选最靠左的条
-        target = bars.reduce((best, cur) => (parsePercent(cur.left) < parsePercent(best.left) ? cur : best), bars[0]);
-    } else {
-        // 右侧:选最靠右的条(left + width 最大)
-        const score = (b) => parsePercent(b.left) + parsePercent(b.width);
-        target = bars.reduce((best, cur) => (score(cur) > score(best) ? cur : best), bars[0]);
-    }
-
-    // 未来(未开始)时,color 为空串;过去/当前一律显示蓝色
-    const hasStarted = !!target?.color;
-    return hasStarted ? "" : "connector-gray";
-};
-
-// 行连接器是否为灰色(用于切换箭头图片)
-const isConnectorGray = (rowIndex) => getConnectorColorClass(rowIndex) === "connector-gray";
-</script>
-
-<style scoped lang="scss">
-.plan-page {
-    width: 100%;
-    height: 100vh;
-    background: #fff;
-    .plan-content {
-        .filter-wrap {
-            background: #fff;
-            padding: 13px 12px;
-            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
-            border-radius: 0 0 20px 20px;
-            .status-filter {
-                background: #fff;
-                padding: 3px 17px;
-                display: flex;
-                align-items: center;
-                gap: 16px;
-                font-size: 12px;
-
-                .status-item {
-                    display: flex;
-                    align-items: center;
-                    justify-content: center;
-                    gap: 6px;
-                    flex: 1;
-                    &.gray {
-                        color: #c4c6c9;
-                        .status-dot {
-                            background-color: #c4c6c9;
-                        }
-                    }
-
-                    &.blue {
-                        color: #2199f8;
-                        .status-dot {
-                            background-color: #2199f8;
-                        }
-                    }
-
-                    &.green {
-                        color: #1ca900;
-                        .status-dot {
-                            background-color: #1ca900;
-                        }
-                    }
-
-                    &.orange {
-                        color: #ff953d;
-                        .status-dot {
-                            background-color: #ff953d;
-                        }
-                    }
-                    .status-dot {
-                        width: 6px;
-                        height: 6px;
-                        border-radius: 50%;
-                    }
-                }
-            }
-
-            .season-tabs {
-                display: flex;
-                gap: 8px;
-                margin-bottom: 12px;
-                .season-tab {
-                    flex: 1;
-                    padding: 7px;
-                    text-align: center;
-                    background: #f3f3f3;
-                    color: #898a8a;
-                    border-radius: 3px;
-                    border: 1px solid transparent;
-                    font-size: 12px;
-                }
-                .season-tab.active {
-                    background: #ffffff;
-                    color: #2199f8;
-                    border-color: #2199f8;
-                }
-            }
-        }
-
-        // 循环时间线样式
-        .cycle-timeline-container {
-            padding: 35px 15px 25px;
-            height: calc(100vh - 135px - 69px - 60px);
-            overflow-y: auto;
-            overflow-x: hidden;
-            .cycle-timeline {
-                position: relative;
-                .cycle-row {
-                    position: relative;
-                    display: flex;
-                    justify-content: space-between;
-                    align-items: center;
-                    margin-bottom: 60px;
-                    padding-right: 30px;
-                    &.odd-index {
-                        padding: 0;
-                        padding-left: 30px;
-                        flex-direction: row-reverse;
-                        .cycle-phenology-wrap {
-                            left: 6px;
-                            width: calc(100% - 13px);
-                        }
-                    }
-
-                    &:last-child {
-                        margin-bottom: 0;
-                        .cycle-phenology-wrap {
-                            left: 20px;
-                            width: calc(100% - 10px);
-                        }
-                    }
-
-                    // 水平时间线
-                    &::before {
-                        content: "";
-                        position: absolute;
-                        top: 0;
-                        left: 6px;
-                        right: 6px;
-                        height: 5px;
-                        border-left: 2px solid #fff;
-                        border-right: 2px solid #fff;
-                        background: #e8e8e8;
-                        transform: translateY(-50%);
-                        z-index: 1;
-                    }
-
-                    .cycle-item {
-                        position: relative;
-                        z-index: 2;
-                        top: 12px;
-
-                        &.term-item {
-                            display: flex;
-                            flex-direction: column;
-                            align-items: center;
-                            top: -11px;
-
-                            .cycle-term-dot {
-                                width: 6px;
-                                height: 6px;
-                                background: #c7c7c7;
-                                border-radius: 50%;
-                                margin-bottom: 4px;
-                            }
-
-                            .cycle-term-label {
-                                font-size: 11px;
-                                color: #c7c7c7;
-                                margin-top: 16px;
-                            }
-
-                            &.active {
-                                .cycle-term-dot {
-                                    background: #858383;
-                                }
-                                .cycle-term-label {
-                                    color: #858383;
-                                }
-                            }
-                        }
-                    }
-
-                    .cycle-phenology-wrap {
-                        position: absolute;
-                        top: -23px;
-                        left: 6px;
-                        width: calc(100% - 10px);
-                        z-index: 3;
-                        height: 100px;
-                        overflow: hidden;
-                        .cycle-label {
-                            position: absolute;
-                            color: #4e4e4e;
-                            font-size: 12px;
-                            min-width: 24px;
-                            height: 20px;
-                            line-height: 20px;
-                            text-align: center;
-                            background: rgba(180, 182, 183, 0.1);
-                            border-bottom: 6px solid #e8e8e8;
-                        }
-                        .cycle-label + .cycle-label {
-                            border-right: 1px solid #fff;
-                        }
-                        .cycle-label.blue {
-                            color: #2199f8;
-                            background: rgba(33, 153, 248, 0.1);
-                            border-bottom-color: #2199f8;
-                        }
-                        .cycle-label.orange {
-                            color: #ff953d;
-                            background: #fff2e7;
-                            border-bottom-color: #ff953d;
-                        }
-                        .arranges {
-                            display: flex;
-                            gap: 8px;
-                            padding-top: 26px;
-                            justify-content: center;
-                            flex-wrap: nowrap;
-                            // 使用与任务框一致的视觉风格
-                            .cycle-task-box {
-                                border: 1px solid rgba(199, 199, 199, 0.5);
-                                border-radius: 2px;
-                                width: 36px;
-                                height: 36px;
-                                min-width: 36px;
-                                line-height: 15px;
-                                font-size: 12px;
-                                box-sizing: border-box;
-                                padding: 2px 0;
-                                text-align: center;
-                                position: relative;
-                                color: #c7c7c7;
-                                .status-icon {
-                                    position: absolute;
-                                    bottom: -10px;
-                                    right: -10px;
-                                }
-                            }
-
-                            .cycle-task-connector {
-                                position: absolute;
-                                top: -4px;
-                                left: 50%;
-                                transform: translateX(-50%);
-                                width: 0;
-                                height: 0;
-                                border-left: 4px solid transparent;
-                                border-right: 4px solid transparent;
-                                border-bottom: 4px solid #dde1e7;
-                            }
-
-                            .cycle-task-box.warning {
-                                border-color: #ff953d;
-                            }
-                            .cycle-task-box.warning .cycle-task-text {
-                                color: #ff953d;
-                            }
-                            .cycle-task-box.warning + .cycle-task-connector,
-                            .cycle-task-box.warning .cycle-task-connector {
-                                border-bottom-color: #ff953d;
-                            }
-
-                            .cycle-task-box.complete {
-                                border-color: #1ca900;
-                            }
-                            .cycle-task-box.complete .cycle-task-text {
-                                color: #1ca900;
-                            }
-                            .cycle-task-box.complete + .cycle-task-connector,
-                            .cycle-task-box.complete .cycle-task-connector {
-                                border-bottom-color: #1ca900;
-                            }
-
-                            .cycle-task-box.normal {
-                                border-color: #2199f8;
-                            }
-                            .cycle-task-box.normal .cycle-task-text {
-                                color: #2199f8;
-                            }
-                            .cycle-task-box.normal + .cycle-task-connector,
-                            .cycle-task-box.normal .cycle-task-connector {
-                                border-bottom-color: #2199f8;
-                            }
-                        }
-                    }
-
-                    .cycle-connector {
-                        position: absolute;
-                        right: 0;
-                        top: 45.5px;
-                        transform: translateY(-50%);
-                        width: 2px;
-                        height: 87px;
-                        border: 5px solid #9dcaf7;
-                        border-left: none;
-                        background: transparent;
-                        img{
-                            width: 13px;
-                            height: 13px;
-                            position: absolute;
-                            top: 50%;
-                            transform: translateY(-50%);
-                            left: -8px;
-                            z-index: 1;
-                        }
-
-                        &.top-connector {
-                            border-top-right-radius: 5px;
-                            border-bottom-right-radius: 5px;
-                            img{
-                                left: -2px;
-                            }
-                        }
-
-                        &.middle-connector {
-                            border-top-left-radius: 5px;
-                            border-bottom-left-radius: 5px;
-                            left: 0;
-                            border-right: none;
-                            border-left: 5px solid #9dcaf7;
-                        }
-
-                        // 动态颜色
-                        &.connector-gray {
-                            border-color: #c4c6c9;
-                        }
-                        &.connector-gray.middle-connector {
-                            border-left-color: #c4c6c9;
-                        }
-                    }
-                }
-            }
-        }
-
-        // 控制区域样式
-        .control-section {
-            position: fixed;
-            width: 100%;
-            left: 0;
-            box-sizing: border-box;
-            bottom: 0px;
-            background: #fff;
-            padding: 16px 12px;
-            display: flex;
-            justify-content: space-between;
-            align-items: center;
-            border-top: 1px solid #f0f0f0;
-
-            .toggle-group {
-                display: flex;
-                align-items: center;
-                gap: 8px;
-
-                .toggle-label {
-                    font-size: 13px;
-                    color: #141414;
-                }
-            }
-
-            .add-button-group {
-                display: flex;
-                align-items: center;
-                gap: 8px;
-                .button {
-                    color: #2199f8;
-                    border-radius: 25px;
-                    padding: 9px 15px;
-                    border: 1px solid #2199f8;
-                }
-                .add-button {
-                    background: linear-gradient(120deg, #76c3ff 0%, #2199f8 100%);
-                    color: white;
-                    border: 1px solid transparent;
-                }
-            }
-        }
-    }
-}
-</style>

+ 22 - 101
src/views/old_mini/recordDetails/index.vue

@@ -93,29 +93,16 @@
             </div>
         </div>
 
-        <popup v-model:show="showUploadProgressPopup" round :close-on-click-overlay="false"
-            class="upload-progress-popup">
-            <div class="upload-progress-title">
-                <span class="label">当前现状:</span>
-                <span class="value">60% 进入红黄叶进程</span>
-            </div>
-            <div class="upload-box" v-loading="popupImageUploadLoading" element-loading-text="上传中...">
-                <div class="box-header">
-                    <div class="upload-title">
-                        <span>上传照片</span>
-                        <span class="optional">(可选)</span>
-                    </div>
-                    <div class="ai-btn">AI 智能分析</div>
+        <UploadProgressPopup ref="uploadProgressPopupRef" v-model:show="showUploadProgressPopup"
+            :popup-image-upload-loading="popupImageUploadLoading" :init-img-arr="initImgArr"
+            @cancel="handleCancelUploadPopup" @confirm="handleConfirmUpload" @handleUpload="handleUploadSuccess">
+            <template #header>
+                <div class="upload-progress-title">
+                    <span class="label">当前现状:</span>
+                    <span class="value">60% 进入红黄叶进程</span>
                 </div>
-                <upload ref="uploadRef" :maxCount="10" :initImgArr="initImgArr" @handleUpload="handleUploadSuccess">
-                </upload>
-                <div class="upload-result">AI识别结果:该病为该病为该病为该病为病为该病为病为该病为</div>
-            </div>
-            <div class="upload-action-btns">
-                <div class="cancel-btn" @click="handleCancelUploadPopup">取消</div>
-                <div class="confirm-btn" @click="handleConfirmUpload">确认信息</div>
-            </div>
-        </popup>
+            </template>
+        </UploadProgressPopup>
     </div>
 </template>
 
@@ -123,11 +110,11 @@
 import customHeader from "@/components/customHeader.vue";
 import PhenologyTrackTimelineItem from "@/components/pageComponents/PhenologyTrackTimelineItem.vue";
 import GrowthStageTimeline from "@/components/pageComponents/GrowthStageTimeline.vue";
+import UploadProgressPopup from "@/components/popup/UploadProgressPopup.vue";
 import { ref, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
-import { Uploader, Popup } from "vant";
+import { Uploader } from "vant";
 import { ElMessage } from "element-plus";
-import upload from "@/components/upload.vue";
 import UploadFile from "@/utils/upliadFile";
 import { getFileExt } from "@/utils/util";
 import IndexMap from "./map/index.js";
@@ -155,7 +142,7 @@ const tabsList = ref([
 
 const showUploadProgressPopup = ref(false);
 const initImgArr = ref([]);
-const uploadRef = ref(null);
+const uploadProgressPopupRef = ref(null);
 const popupImageUploadLoading = ref(false);
 const uploadFileObj = new UploadFile();
 const miniUserId = localStorage.getItem("MINI_USER_ID");
@@ -164,7 +151,7 @@ const beforeReadUpload = (file) => {
     showUploadProgressPopup.value = true;
     initImgArr.value = [];
     popupImageUploadLoading.value = false;
-    uploadRef.value && uploadRef.value.uploadReset();
+    uploadProgressPopupRef.value?.uploadReset();
     return true;
 };
 
@@ -523,82 +510,16 @@ onMounted(() => {
     }
 }
 
-.upload-progress-popup {
-    width: 100%;
-    padding: 24px 16px;
-    background: linear-gradient(360deg, #FFFFFF 74.2%, #D1EBFF 100%);
-
-    .upload-progress-title {
-        color: rgba(60, 60, 60, 0.5);
-        font-weight: 500;
-        background: rgba(33, 153, 248, 0.1);
-        border-radius: 5px;
-        padding: 5px 10px;
-        margin-bottom: 12px;
-
-        .value {
-            color: #2199F8;
-        }
-    }
-
-    .upload-box {
-        margin-bottom: 12px;
-        position: relative;
-        min-height: 88px;
-        .box-header{
-            display: flex;
-            align-items: center;
-            justify-content: space-between;
-            margin-bottom: 12px;
-            .upload-title{
-                font-size: 16px;
-                .optional{
-                    font-size: 12px;
-                    color: rgba(18, 18, 18, 0.2);
-                }
-            }
-            .ai-btn{
-                padding: 5px 10px;
-                border-radius: 4px;
-                background: rgba(33, 153, 248, 0.1);
-                color: #2199F8;
-                border: 1px solid #2199F8;
-                opacity: 0.5;
-            }
-        }
-        .upload-result{
-            color: #646464;
-            padding: 6px 10px;
-            background: rgba(161, 161, 161, 0.1);
-            border-radius: 5px;
-            margin-top: 12px;
-        }
-    }
-
-    .upload-action-btns {
-        display: flex;
-        gap: 10px;
-        margin-top: 16px;
-
-        .cancel-btn,
-        .confirm-btn {
-            flex: 1;
-            border-radius: 4px;
-            padding: 8px;
-            text-align: center;
-            font-size: 16px;
-        }
-
-        .cancel-btn {
-            border: 1px solid #dcdfe6;
-            color: #606266;
-            background: #ffffff;
-        }
+.upload-progress-title {
+    color: rgba(60, 60, 60, 0.5);
+    font-weight: 500;
+    background: rgba(33, 153, 248, 0.1);
+    border-radius: 5px;
+    padding: 5px 10px;
+    margin-bottom: 12px;
 
-        .confirm-btn {
-            background: #2199f8;
-            color: #ffffff;
-        }
+    .value {
+        color: #2199F8;
     }
 }
 </style>

+ 136 - 17
src/views/old_mini/recordDetails/mapManage.vue

@@ -4,11 +4,7 @@
         <div class="map-manage-content">
             <locationSearch class="location-search" @change="handleLocationChange"></locationSearch>
             <div class="map-container" ref="mapContainer"></div>
-            <div
-                class="new-region-btn"
-                v-show="!drawingEnabled"
-                @click="onStartRegionDrawing"
-            >新建管理分区</div>
+            <div class="new-region-btn" v-show="!drawingEnabled" @click="onStartRegionDrawing">新建管理分区</div>
             <div class="map-icon" v-show="drawingEnabled" @click="handleMapIconClick">
                 <img src="@/assets/img/map/map-icon.png" alt="">
             </div>
@@ -20,12 +16,39 @@
             </template>
             <div v-else class="bottom-btn secondary-btn">邀请勾画</div>
         </div>
+
+        <UploadProgressPopup ref="uploadProgressPopupRef" v-model:show="showUploadProgressPopup"
+            :popup-image-upload-loading="popupImageUploadLoading" :init-img-arr="initImgArr"
+            @cancel="handleCancelUploadPopup" @confirm="handleConfirmUpload" @handleUpload="handleUploadSuccess"
+            confirm-text="点击上传">
+            <template #header>
+                <div class="upload-form">
+                    <div class="form-item">
+                        <div class="item-label">区域名称</div>
+                        <el-input v-model="formData.regionName" size="large" placeholder="请输入区域名称" />
+                    </div>
+                    <div class="form-item">
+                        <div class="item-label">品种 <span class="optional">(多选)</span></div>
+                        <el-select v-model="value" placeholder="选择品种" size="large">
+                            <el-option v-for="item in options" :key="item.value" :label="item.label"
+                                :value="item.value" />
+                        </el-select>
+                    </div>
+                    <div class="form-item">
+                        <div class="item-label">物候时间</div>
+                        <GrowthStageTimeline v-model="growthStageIndex" :stages="growthStages" />
+                    </div>
+                </div>
+            </template>
+        </UploadProgressPopup>
     </div>
 </template>
 
 <script setup>
 import customHeader from "@/components/customHeader.vue";
 import locationSearch from "@/components/pageComponents/locationSearch.vue";
+import GrowthStageTimeline from "@/components/pageComponents/GrowthStageTimeline.vue";
+import UploadProgressPopup from "@/components/popup/UploadProgressPopup.vue";
 import MapManage from "./map/mapManage.js";
 import { ref, onMounted } from "vue";
 import { ElMessageBox, ElMessage } from "element-plus";
@@ -38,6 +61,73 @@ const mapManage = new MapManage();
 const mapContainer = ref(null);
 const drawingEnabled = ref(false);
 
+const showUploadProgressPopup = ref(false);
+const initImgArr = ref([]);
+const uploadProgressPopupRef = ref(null);
+const popupImageUploadLoading = ref(false);
+const uploadData = ref([]);
+const formData = ref({
+    regionName: '',
+});
+
+const options = ref([
+    {
+        value: '1',
+        label: '品种1',
+    },
+    {
+        value: '2',
+        label: '品种2',
+    },
+]);
+
+const growthStageIndex = ref();
+const growthStages = ref([
+    {
+        label: "5%萌动",
+        tags: ["最佳给肥点"],
+        periodTitle: "生育期",
+        periodSubtitle: "描述的小字描述的",
+    },
+    {
+        label: "30%展开",
+        periodTitle: "生育期",
+        periodSubtitle: "描述的小字描述的",
+    },
+    {
+        label: "60%展开",
+        tags: ["最佳防治点", "最佳追肥点"],
+        periodTitle: "生育期",
+        periodSubtitle: "描述的小字描述的",
+    },
+    {
+        label: "30%红黄",
+        periodTitle: "生育期",
+        periodSubtitle: "描述的小字描述的",
+    },
+    {
+        label: "60%红黄",
+        periodTitle: "生育期",
+        periodSubtitle: "描述的小字描述的",
+    },
+]);
+
+const handleCancelUploadPopup = () => {
+    showUploadProgressPopup.value = false;
+};
+
+const handleConfirmUpload = () => {
+    if (!uploadData.value || uploadData.value.length === 0) {
+        ElMessage.warning("请先上传照片");
+        return;
+    }
+    showUploadProgressPopup.value = false;
+};
+
+const handleUploadSuccess = (data) => {
+    uploadData.value = data.imgArr;
+};
+
 const onStartRegionDrawing = () => {
     drawingEnabled.value = true;
     mapManage.enableRegionDrawing();
@@ -61,20 +151,24 @@ const handleClearDraw = () => {
         .then(() => {
             mapManage.clearLayer();
         })
-        .catch(() => {});
+        .catch(() => { });
 };
 
 const handleConfirmDraw = () => {
-    const payload = mapManage.getAreaGeometry();
-    if (!payload.geometryArr.length) {
-        ElMessage.warning("请先勾画地块");
-        return;
-    }
-    console.log("地块数据", payload);
-    console.log("WKT 列表", payload.geometryArr);
-    console.log("合计亩数", payload.mianji);
-    console.log("分块明细", payload.parcels);
-    router.back();
+    // const payload = mapManage.getAreaGeometry();
+    // if (!payload.geometryArr.length) {
+    //     ElMessage.warning("请先勾画地块");
+    //     return;
+    // }
+    // console.log("地块数据", payload);
+    // console.log("WKT 列表", payload.geometryArr);
+    // console.log("合计亩数", payload.mianji);
+    // console.log("分块明细", payload.parcels);
+    // router.back();
+    initImgArr.value = [];
+    popupImageUploadLoading.value = false;
+    uploadProgressPopupRef.value?.uploadReset?.();
+    showUploadProgressPopup.value = true;
 };
 
 onMounted(() => {
@@ -86,9 +180,11 @@ onMounted(() => {
 .map-manage {
     width: 100%;
     height: 100vh;
+
     .map-manage-content {
         height: calc(100% - 120px);
         position: relative;
+
         .location-search {
             width: calc(100% - 24px);
             position: absolute;
@@ -96,10 +192,12 @@ onMounted(() => {
             left: 12px;
             z-index: 2;
         }
+
         .map-container {
             width: 100%;
             height: 100%;
         }
+
         .map-icon {
             position: absolute;
             width: 35px;
@@ -112,12 +210,14 @@ onMounted(() => {
             display: flex;
             align-items: center;
             justify-content: center;
+
             img {
                 width: 17px;
                 height: 19px;
             }
         }
-        .new-region-btn{
+
+        .new-region-btn {
             position: absolute;
             top: 50%;
             left: 50%;
@@ -129,15 +229,34 @@ onMounted(() => {
             padding: 10px 24px;
         }
     }
+
     .custom-bottom-fixed-btns {
         justify-content: space-between;
+
         .bottom-btn {
             padding: 10px 20px;
         }
+
         .primary-btn {
             background: #2199F8;
             color: #fff;
         }
     }
+
+    .upload-form {
+        .form-item {
+            margin-bottom: 10px;
+
+            .item-label {
+                font-size: 16px;
+                margin-bottom: 8px;
+
+                .optional {
+                    font-size: 14px;
+                    color: rgba(0, 0, 0, 0.2);
+                }
+            }
+        }
+    }
 }
 </style>