单应用项目,可以创建很多独立工具类页面 ,不用登录 初始化的页面
zhangchen
2025-07-24 d5f06dbd22de83d9ecbd5ad70bc37decda91bb4d
src/views/mobile/bedsideAuxiliaryScreen/components/TimePicker.vue
@@ -1,119 +1,228 @@
<template>
  <div class="time-picker">
    <div class="picker-column" ref="hourRef" @scroll="onScroll('hour')">
      <div v-for="h in hours" :key="h" class="picker-item" :class="{ active: h === selectedHour }">{{ h.toString().padStart(2, '0') }}</div>
      <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 in minutes" :key="m" class="picker-item" :class="{ active: m === selectedMinute }">{{ m.toString().padStart(2, '0') }}</div>
      <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'
import { ref, watch, onMounted, nextTick } from "vue";
interface Props {
  modelValue: string // 格式为 "HH:mm"
}
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits(["update:modelValue"]);
const selectedHour = ref(0)
const selectedMinute = ref(0)
const hourRef = ref<HTMLDivElement | null>(null)
const minuteRef = ref<HTMLDivElement | null>(null)
const hours = Array.from({ length: 24 }, (_, i) => i);
const minutes = Array.from({ length: 60 }, (_, i) => i);
const hours = Array.from({ length: 24 }, (_, i) => i)
const minutes = Array.from({ length: 60 }, (_, i) => i)
function scrollTo(refEl: HTMLDivElement | null, index: number) {
  if (!refEl) return
  refEl.scrollTo({ top: index * 40, behavior: 'smooth' })
// 为循环滚动,前面后面都各补两个,要不要选不中最后一个
function createLoopArray(arr: number[]) {
  return [...arr.slice(-2), ...arr, ...arr.slice(0, 2)];
}
function updateModel() {
  const value = `${selectedHour.value.toString().padStart(2, '0')}:${selectedMinute.value.toString().padStart(2, '0')}`
  emit('update:modelValue', value)
}
const loopHours = createLoopArray(hours);
const loopMinutes = createLoopArray(minutes);
function onScroll(type: 'hour' | 'minute') {
  const el = type === 'hour' ? hourRef.value : minuteRef.value
  if (!el) return
const hourRef = ref<HTMLElement | null>(null);
const minuteRef = ref<HTMLElement | null>(null);
  clearTimeout((el as any)._timer)
  ;(el as any)._timer = setTimeout(() => {
    const index = Math.round(el.scrollTop / 40)
    if (type === 'hour') {
      selectedHour.value = hours[index]
      scrollTo(hourRef.value, index)
    } else {
      selectedMinute.value = minutes[index]
      scrollTo(minuteRef.value, index)
    }
    updateModel()
  }, 100)
}
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(
  () => props.modelValue,
  (newVal) => {
    if (!newVal) return
    const [h, m] = newVal.split(':').map(Number)
    selectedHour.value = h
    selectedMinute.value = m
    nextTick(() => {
      scrollTo(hourRef.value, h)
      scrollTo(minuteRef.value, m)
    })
  },
  { immediate: true }
)
  () =>
    `${selectedHour.value.toString().padStart(2, "0")}:${selectedMinute.value
      .toString()
      .padStart(2, "0")}`,
  (val) => {
    emit("update:modelValue", val);
  }
);
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>
<style scoped lang="less">
.time-picker {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 200px;
  background: #e6efff;
  font-family: sans-serif;
}
  justify-content: center;
  height: 2rem; // 5 * 0.4rem 每个item高度0.4rem
  overflow: hidden;
.picker-column {
  width: 60px;
  height: 200px;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  -webkit-overflow-scrolling: touch;
  text-align: center;
  position: relative;
  padding-top: 80px;
  padding-bottom: 80px;
}
  .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: 40px;
  line-height: 40px;
  font-size: 14px;
  color: #666;
  scroll-snap-align: center;
  transition: all 0.2s;
  transform: scale(0.8);
  opacity: 0.5;
}
    .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;
.picker-item.active {
  font-size: 24px;
  font-weight: bold;
  color: #333;
  transform: scale(1.2);
  opacity: 1;
}
.colon {
  font-size: 24px;
  margin: 0 10px;
      &.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>