From 64aaf44b6b2948631ebd0d9840d51e5e31ae5479 Mon Sep 17 00:00:00 2001
From: zhangchen <1652267879@qq.com>
Date: 星期五, 25 七月 2025 01:44:25 +0800
Subject: [PATCH] Merge branch 'ID1825-床旁副屏改版' into test

---
 src/views/mobile/bedsideAuxiliaryScreen/components/TimePicker.vue |  248 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 248 insertions(+), 0 deletions(-)

diff --git a/src/views/mobile/bedsideAuxiliaryScreen/components/TimePicker.vue b/src/views/mobile/bedsideAuxiliaryScreen/components/TimePicker.vue
new file mode 100644
index 0000000..1f1376f
--- /dev/null
+++ b/src/views/mobile/bedsideAuxiliaryScreen/components/TimePicker.vue
@@ -0,0 +1,248 @@
+<template>
+  <div class="time-picker">
+    <div class="picker-column" ref="hourRef" @scroll="onScroll('hour')">
+      <div
+        v-for="(h, index) in loopHours"
+        :key="index"
+        class="picker-item hours"
+        :class="getClassByIndex(index, selectedIndexHour)"
+        :style="getStyleByIndex(index, selectedIndexHour)"
+      >
+        {{ h.toString().padStart(2, "0") }}
+      </div>
+    </div>
+    <span class="colon">:</span>
+    <div class="picker-column" ref="minuteRef" @scroll="onScroll('minute')">
+      <div
+        v-for="(m, index) in loopMinutes"
+        :key="index"
+        class="picker-item minutes"
+        :class="getClassByIndex(index, selectedIndexMinute)"
+        :style="getStyleByIndex(index, selectedIndexMinute)"
+      >
+        {{ m.toString().padStart(2, "0") }}
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, onMounted, nextTick } from "vue";
+
+const props = defineProps<{ modelValue: string }>();
+const emit = defineEmits(["update:modelValue"]);
+
+const hours = Array.from({ length: 24 }, (_, i) => i);
+const minutes = Array.from({ length: 60 }, (_, i) => i);
+
+// 为循环滚动,前面后面都各补两个,要不要选不中最后一个
+function createLoopArray(arr: number[]) {
+  return [...arr.slice(-2), ...arr, ...arr.slice(0, 2)];
+}
+
+const loopHours = createLoopArray(hours);
+const loopMinutes = createLoopArray(minutes);
+
+const hourRef = ref<HTMLElement | null>(null);
+const minuteRef = ref<HTMLElement | null>(null);
+
+const ITEM_REM = 0.4;
+const itemHeight = remToPx(ITEM_REM);
+
+const selectedIndexHour = ref(2);
+const selectedIndexMinute = ref(2);
+
+const selectedHour = ref(hours[0]);
+const selectedMinute = ref(minutes[0]);
+
+// 标记程序主动滚动,防止滚动事件死循环
+const isProgrammaticScroll = {
+  hour: false,
+  minute: false,
+};
+
+// 初始化滚动,选中传入时间
+onMounted(() => {
+  const [h, m] = props.modelValue.split(":").map(Number);
+  const hourIdx = hours.indexOf(h);
+  const minuteIdx = minutes.indexOf(m);
+
+  selectedIndexHour.value = hourIdx === -1 ? 2 : hourIdx + 2;
+  selectedIndexMinute.value = minuteIdx === -1 ? 2 : minuteIdx + 2;
+
+  selectedHour.value = h;
+  selectedMinute.value = m;
+
+  nextTick(() => {
+    scrollToSelected("hour", selectedIndexHour.value);
+    scrollToSelected("minute", selectedIndexMinute.value);
+  });
+});
+
+watch(
+  () =>
+    `${selectedHour.value.toString().padStart(2, "0")}:${selectedMinute.value
+      .toString()
+      .padStart(2, "0")}`,
+  (val) => {
+    emit("update:modelValue", val);
+  }
+);
+
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    const [h, m] = newVal.split(":").map(Number);
+    const hourIdx = hours.indexOf(h);
+    const minuteIdx = minutes.indexOf(m);
+
+    if (hourIdx !== -1) {
+      selectedIndexHour.value = hourIdx + 2;
+      selectedHour.value = h;
+      scrollToSelected("hour", selectedIndexHour.value);
+    }
+
+    if (minuteIdx !== -1) {
+      selectedIndexMinute.value = minuteIdx + 2;
+      selectedMinute.value = m;
+      scrollToSelected("minute", selectedIndexMinute.value);
+    }
+  }
+);
+
+function onScroll(type: "hour" | "minute") {
+  if (isProgrammaticScroll[type]) {
+    // 程序滚动,忽略,避免死循环
+    isProgrammaticScroll[type] = false;
+    return;
+  }
+
+  const refEl = type === "hour" ? hourRef.value : minuteRef.value;
+  if (!refEl) return;
+
+  const scrollTop = refEl.scrollTop;
+  let index = Math.round(scrollTop / itemHeight + 2);
+
+  const arrLen = type === "hour" ? hours.length : minutes.length;
+
+  if (index < 2) {
+    isProgrammaticScroll[type] = true;
+    refEl.scrollTop = itemHeight * (arrLen + (index - 2));
+    index = arrLen + (index - 2);
+  } else if (index > arrLen + 1) {
+    isProgrammaticScroll[type] = true;
+    refEl.scrollTop = itemHeight * (index - arrLen - 2);
+    index = index - arrLen - 2;
+  }
+
+  if (type === "hour") {
+    selectedIndexHour.value = index;
+    selectedHour.value = loopHours[index];
+  } else {
+    selectedIndexMinute.value = index;
+    selectedMinute.value = loopMinutes[index];
+  }
+}
+
+function scrollToSelected(type: "hour" | "minute", index: number) {
+  const refEl = type === "hour" ? hourRef.value : minuteRef.value;
+  if (!refEl) return;
+
+  isProgrammaticScroll[type] = true;
+  refEl.scrollTo({
+    top: (index - 2) * itemHeight,
+    behavior: "auto",
+  });
+}
+
+function getClassByIndex(index: number, selectedIndex: number) {
+  const diff = Math.abs(index - selectedIndex);
+  return {
+    active: diff === 0,
+    medium: diff === 1,
+    small: diff === 2,
+  };
+}
+
+function getStyleByIndex(index: number, selectedIndex: number) {
+  const diff = Math.min(Math.abs(index - selectedIndex), 2);
+  return {
+    opacity: diff === 2 ? 0.4 : 1,
+    zIndex: 10 - diff,
+    transition: "opacity 0.3s ease",
+  };
+}
+
+function remToPx(rem: number) {
+  return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
+}
+</script>
+
+<style scoped lang="less">
+.time-picker {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 2rem; // 5 * 0.4rem 每个item高度0.4rem
+  overflow: hidden;
+
+  .picker-column {
+    height: 2rem;
+    width: 0.9rem;
+    overflow-y: scroll;
+    scroll-snap-type: y mandatory;
+    -webkit-overflow-scrolling: touch;
+    scrollbar-width: none; /* Firefox */
+    -ms-overflow-style: none; /* IE 10+ */
+    &::-webkit-scrollbar {
+      display: none; /* Chrome Safari */
+    }
+
+    .picker-item {
+      height: 0.4rem;
+      line-height: 0.4rem;
+      text-align: center;
+      font-size: 0.24rem;
+      scroll-snap-align: center;
+      user-select: none;
+      cursor: pointer;
+      transition: color 0.3s ease, font-size 0.3s ease;
+
+      &.active {
+        font-size: 0.5rem;
+        font-weight: 700;
+        color: #111;
+        &.hours {
+          text-align: left;
+        }
+        &.minutes {
+          text-align: right;
+        }
+      }
+      &.medium {
+        font-size: 0.3rem;
+        color: #666;
+        &.hours {
+          text-align: left;
+          padding-left: 0.2rem;
+        }
+        &.minutes {
+          text-align: right;
+          padding-right: 0.2rem;
+        }
+      }
+      &.small {
+        font-size: 0.24rem;
+        color: #aaa;
+      }
+    }
+  }
+
+  .colon {
+    font-size: 0.5rem;
+    font-weight: 600;
+    color: #444;
+    user-select: none;
+  }
+}
+</style>

--
Gitblit v1.8.0