Parcourir la source

feat:对接时间轴接口

lxf il y a 2 semaines
Parent
commit
aaafd130db

+ 111 - 28
src/components/pageComponents/GrowthStageTimeline.vue

@@ -2,27 +2,23 @@
     <div class="growth-stage-timeline">
         <div class="growth-stage-timeline__scroll" ref="scrollRef">
             <div class="growth-stage-timeline__inner" :style="innerStyle">
-                <!-- 生育期背景 -->
+                <!-- 生育期背景:同一 period 连续多列合并为一格,标题与描述跨列居中 -->
                 <div
                     class="growth-stage-timeline__bg"
                     :style="{ gridTemplateColumns: gridCols }"
                 >
                     <div
-                        v-for="(stage, i) in normalizedStages"
-                        :key="'bg-' + i"
-                        class="growth-stage-timeline__bg-cell"
-                        :class="{
-                            'growth-stage-timeline__bg-cell--start': isPeriodStart(i),
-                        }"
+                        v-for="(run, ri) in periodRuns"
+                        :key="'bg-run-' + ri"
+                        class="growth-stage-timeline__bg-cell growth-stage-timeline__bg-cell--period"
+                        :style="{ gridColumn: `span ${run.span}` }"
                     >
-                        <template v-if="isPeriodStart(i)">
-                            <div class="growth-stage-timeline__period-title">
-                                {{ stage.periodTitle }}
-                            </div>
-                            <div class="growth-stage-timeline__period-sub">
-                                {{ stage.periodSubtitle }}
-                            </div>
-                        </template>
+                        <div class="growth-stage-timeline__period-title">
+                            {{ run.periodTitle }}
+                        </div>
+                        <div class="growth-stage-timeline__period-sub">
+                            {{ run.periodSubtitle }}
+                        </div>
                     </div>
                 </div>
 
@@ -150,7 +146,7 @@ const props = defineProps({
     },
 });
 
-const emit = defineEmits(["update:modelValue", "change"]);
+const emit = defineEmits(["update:modelValue", "change", "scrollSettled"]);
 
 const scrollRef = ref(null);
 const trackRef = ref(null);
@@ -192,11 +188,37 @@ function periodKey(stage) {
     return `${s.periodTitle || ""}\0${s.periodSubtitle || ""}`;
 }
 
-function isPeriodStart(i) {
-    if (i === 0) return true;
+/** 连续相同生育期合并为一段,用于背景区 grid-column span */
+const periodRuns = computed(() => {
     const list = normalizedStages.value;
-    return periodKey(list[i]) !== periodKey(list[i - 1]);
-}
+    if (!list.length) {
+        return [
+            {
+                span: 1,
+                periodTitle: "",
+                periodSubtitle: "",
+            },
+        ];
+    }
+    const runs = [];
+    let i = 0;
+    while (i < list.length) {
+        const key = periodKey(list[i]);
+        let span = 1;
+        let j = i + 1;
+        while (j < list.length && periodKey(list[j]) === key) {
+            span++;
+            j++;
+        }
+        runs.push({
+            span,
+            periodTitle: list[i].periodTitle ?? "",
+            periodSubtitle: list[i].periodSubtitle ?? "",
+        });
+        i = j;
+    }
+    return runs;
+});
 
 function clampStageIndex(v) {
     const last = Math.max(0, colCount.value - 1);
@@ -427,20 +449,72 @@ function onPointerUp(e) {
     }
     movedDuringHandleDrag = false;
     emit("update:modelValue", nextIdx);
-    emit("change", nextIdx, normalizedStages.value[nextIdx]);
+    armScrollSettledAfterHandle();
+}
+
+/** 手柄松开后待「横向滚动结束」再向父组件派发 change / scrollSettled */
+let pendingScrollSettled = false;
+let scrollSettledFallbackTimer = null;
+let scrollSettledDebounceTimer = null;
+
+function clearScrollSettledTimers() {
+    if (scrollSettledFallbackTimer != null) {
+        clearTimeout(scrollSettledFallbackTimer);
+        scrollSettledFallbackTimer = null;
+    }
+    if (scrollSettledDebounceTimer != null) {
+        clearTimeout(scrollSettledDebounceTimer);
+        scrollSettledDebounceTimer = null;
+    }
+}
+
+function emitScrollSettledIfPending() {
+    if (!pendingScrollSettled) return;
+    pendingScrollSettled = false;
+    clearScrollSettledTimers();
+    const idx = activeIndex.value;
+    const stage = normalizedStages.value[idx];
+    emit("change", idx, stage);
+    emit("scrollSettled", idx, stage);
+}
+
+function armScrollSettledAfterHandle() {
+    pendingScrollSettled = true;
+    clearScrollSettledTimers();
+    scrollSettledFallbackTimer = setTimeout(() => {
+        scrollSettledFallbackTimer = null;
+        emitScrollSettledIfPending();
+    }, 200);
 }
 
 function onScrollAreaScroll() {
     if (showHandleTooltip.value) clampTooltipToScrollArea();
+    if (!pendingScrollSettled) return;
+    if (scrollSettledDebounceTimer != null) {
+        clearTimeout(scrollSettledDebounceTimer);
+    }
+    scrollSettledDebounceTimer = setTimeout(() => {
+        scrollSettledDebounceTimer = null;
+        emitScrollSettledIfPending();
+    }, 80);
+}
+
+function onScrollAreaScrollEnd() {
+    emitScrollSettledIfPending();
+}
+
+function onWindowResize() {
+    if (showHandleTooltip.value) clampTooltipToScrollArea();
 }
 
 onMounted(() => {
     window.addEventListener("pointermove", onPointerMove);
     window.addEventListener("pointerup", onPointerUp);
     window.addEventListener("pointercancel", onPointerUp);
-    window.addEventListener("resize", onScrollAreaScroll);
+    window.addEventListener("resize", onWindowResize);
     const el = scrollRef.value;
     el?.addEventListener("scroll", onScrollAreaScroll, { passive: true });
+    el?.addEventListener("scrollend", onScrollAreaScrollEnd, { passive: true });
     el?.addEventListener("wheel", onTimelineWheel, { passive: false });
     nextTick(() => clampTooltipToScrollArea());
     el?.addEventListener("touchstart", onScrollAreaTouchStart, {
@@ -470,14 +544,17 @@ onBeforeUnmount(() => {
     window.removeEventListener("pointermove", onPointerMove);
     window.removeEventListener("pointerup", onPointerUp);
     window.removeEventListener("pointercancel", onPointerUp);
-    window.removeEventListener("resize", onScrollAreaScroll);
+    window.removeEventListener("resize", onWindowResize);
     const el = scrollRef.value;
     el?.removeEventListener("scroll", onScrollAreaScroll);
+    el?.removeEventListener("scrollend", onScrollAreaScrollEnd);
     el?.removeEventListener("wheel", onTimelineWheel);
     el?.removeEventListener("touchstart", onScrollAreaTouchStart);
     el?.removeEventListener("touchmove", onScrollAreaTouchMove);
     resizeObserver?.disconnect();
     resizeObserver = null;
+    clearScrollSettledTimers();
+    pendingScrollSettled = false;
 });
 </script>
 
@@ -504,42 +581,48 @@ onBeforeUnmount(() => {
 
 .growth-stage-timeline__bg {
     display: grid;
-    min-height: 56px;
+    align-items: stretch;
     border-radius: 6px 6px 0 0;
     overflow: hidden;
 }
 
 .growth-stage-timeline__bg-cell {
     background: #f0f2f5;
-    padding: 8px 6px;
+    padding: 8px 10px;
     text-align: center;
     box-sizing: border-box;
     border-right: 1px solid rgba(0, 0, 0, 0.04);
+    min-width: 0;
 
     &:last-child {
         border-right: none;
     }
 
-    &--start {
+    &--period {
         display: flex;
         flex-direction: column;
         align-items: center;
         justify-content: center;
-        gap: 2px;
+        gap: 4px;
     }
 }
 
 .growth-stage-timeline__period-title {
-    font-size: 15px;
+    font-size: 14px;
     font-weight: 600;
     color: #1d2129;
     line-height: 1.2;
+    text-align: center;
+    max-width: 100%;
 }
 
 .growth-stage-timeline__period-sub {
     font-size: 11px;
     color: rgba(60, 60, 60, 0.45);
     line-height: 1.3;
+    text-align: center;
+    max-width: 100%;
+    word-break: break-word;
 }
 
 .growth-stage-timeline__track {

+ 42 - 21
src/views/old_mini/recordDetails/index.vue

@@ -123,7 +123,12 @@
                             <span class="current-status">当前现状:当前60%进入到红黄叶进程</span>
                         </div>
                         <div class="time-line">
-                            <GrowthStageTimeline v-model="growthStageIndex" :stages="growthStages" />
+                            <GrowthStageTimeline
+                                v-model="growthStageIndex"
+                                :stages="growthStages"
+                                @scroll-settled="onStageScrollSettled"
+                                />
+                            <!-- <GrowthStageTimeline v-model="growthStageIndex" :stages="growthStages" /> -->
                         </div>
                         <div class="confirm-btn-wrap">
                             <uploader @click="handleUploadClick" :before-read="beforeReadUpload" class="upload-wrap"
@@ -296,6 +301,12 @@ const beforeReadUpload = (file) => {
     return true;
 };
 
+function onStageScrollSettled(index, stage) {
+    console.log('index',index)
+    console.log('stage',stage)
+  // index:当前节点下标;stage:当前项(含 label、tags、periodTitle 等)
+}
+
 const afterReadUpload = async (data) => {
     initImgArr.value = [];
     if (!Array.isArray(data)) {
@@ -430,30 +441,40 @@ const getFarmRecord = async () => {
     }
 }
 
-function transformToKeyValueArray(obj) {
-  return Object.entries(obj).map(([key, value]) => ({ key, value }));
+function normalizeTimeDescribe(raw) {
+    if (Array.isArray(raw)) return raw;
+    if (typeof raw === 'string' && raw.trim()) {
+        try {
+            const parsed = JSON.parse(raw);
+            return Array.isArray(parsed) ? parsed : [];
+        } catch {
+            return [];
+        }
+    }
+    return [];
 }
 
-const getFindPhenologyInfo = async () =>{
-    const cropType = JSON.parse(localStorage.getItem('selectedFarmData')).farm_variety
-    const res = await VE_API.monitor.getFindPhenologyInfo({ crop_type:cropType });
-    console.log(res)
-    if (res.code === 200 && res.data.length) {
-        growthStages.value = res.data.map(item => {
-            const discribe = JSON.parse(item.time_discribe)
-            console.log(discribe)
-            const result = transformToKeyValueArray(discribe);
-            console.log(result)
-            
-            return {
-                label:result,
-                tags:[],
-                periodTitle:item.phenophase_name,
-                periodSubtitle:item.phenophase_discribe
+const getFindPhenologyInfo = async () => {
+    const cropType = JSON.parse(localStorage.getItem('selectedFarmData')).farm_variety;
+    const res = await VE_API.monitor.getFindPhenologyInfo({ crop_type: cropType });
+    if (res.code === 200 && Array.isArray(res.data) && res.data.length) {
+        const flat = [];
+        for (const item of res.data) {
+            const milestones = normalizeTimeDescribe(item.time_discribe);
+            for (const t of milestones) {
+                flat.push({
+                    label: t.label ?? '',
+                    tags: Array.isArray(t.tags) ? t.tags : [],
+                    periodTitle: item.phenophase_name ?? '',
+                    periodSubtitle: item.phenophase_discribe ?? '',
+                });
             }
-        })
+        }
+        if (flat.length) {
+            growthStages.value = flat;
+        }
     }
-}
+};
 
 </script>