From d5f06dbd22de83d9ecbd5ad70bc37decda91bb4d Mon Sep 17 00:00:00 2001
From: zhangchen <1652267879@qq.com>
Date: 星期四, 24 七月 2025 23:15:08 +0800
Subject: [PATCH] ID1625-时间选择组件完成
---
src/views/mobile/bedsideAuxiliaryScreen/components/TimePicker.vue | 285 +++++++++++++++++++++++++++++++++++++++-----------------
1 files changed, 197 insertions(+), 88 deletions(-)
diff --git a/src/views/mobile/bedsideAuxiliaryScreen/components/TimePicker.vue b/src/views/mobile/bedsideAuxiliaryScreen/components/TimePicker.vue
index 74479f7..4a18241 100644
--- a/src/views/mobile/bedsideAuxiliaryScreen/components/TimePicker.vue
+++ b/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>
--
Gitblit v1.8.0