单应用项目,可以创建很多独立工具类页面 ,不用登录 初始化的页面
zhangchen
2025-07-25 64aaf44b6b2948631ebd0d9840d51e5e31ae5479
src/views/mobile/bedsideAuxiliaryScreen/components/TimePicker.vue
New file
@@ -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>