|
|
@@ -0,0 +1,135 @@
|
|
|
+<template>
|
|
|
+ <view class="ruler-wrapper" v-show="visible">
|
|
|
+ <scroll-view
|
|
|
+ class="ruler-scroll"
|
|
|
+ scroll-x
|
|
|
+ :scroll-left="scrollLeft"
|
|
|
+ :scroll-with-animation="useAnimation"
|
|
|
+ @scroll="onScroll"
|
|
|
+ @touchend="onTouchEnd"
|
|
|
+ @touchcancel="onTouchEnd"
|
|
|
+ scroll-into-view=""
|
|
|
+ >
|
|
|
+ <view class="ruler-inner" :style="{ width: totalWidth + 'px' }">
|
|
|
+ <view
|
|
|
+ v-for="(g, idx) in gridList"
|
|
|
+ :key="idx"
|
|
|
+ class="grid-item"
|
|
|
+ :class="{ long: g.isLongGrid }"
|
|
|
+ :style="g.showText ? { height: '40px' } : gridItemStyle"
|
|
|
+ >
|
|
|
+ <text v-if="g.showText" class="grid-num">{{ g.displayNum }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ <view class="indicator"></view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed, watch, onMounted } from 'vue'
|
|
|
+import { getCurrentInstance } from 'vue'
|
|
|
+
|
|
|
+interface GridItem { num: number; displayNum: number | string; isLongGrid: boolean; showText: boolean }
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ min: { type: Number, default: 0 },
|
|
|
+ max: { type: Number, default: 100 },
|
|
|
+ step: { type: Number, default: 1 }, // value step
|
|
|
+ unit: { type: String, default: '' },
|
|
|
+ initialValue: { type: Number, default: 0 },
|
|
|
+ gutter: { type: Number, default: 10 }, // px per unit
|
|
|
+ visible: { type: Boolean, default: true },
|
|
|
+ useAnimation: { type: Boolean, default: true }
|
|
|
+})
|
|
|
+
|
|
|
+const emit = defineEmits(['update:value', 'change'])
|
|
|
+
|
|
|
+const instance = getCurrentInstance()
|
|
|
+const scrollLeft = ref(0)
|
|
|
+const actualScrollLeft = ref(0)
|
|
|
+
|
|
|
+// prepare grid
|
|
|
+const totalUnits = computed(() => Math.round((props.max - props.min) / props.step))
|
|
|
+
|
|
|
+// extra grids to fill screen on both sides
|
|
|
+const extraGridCount = Math.ceil((uni?.getSystemInfoSync?.().windowWidth || 375) / props.gutter)
|
|
|
+
|
|
|
+const gridList = ref<GridItem[]>([])
|
|
|
+const totalWidth = ref(0)
|
|
|
+
|
|
|
+const gridItemStyle = computed(() => ({ width: props.gutter + 'px', height: '24px' }))
|
|
|
+
|
|
|
+function buildGrid() {
|
|
|
+ const count = totalUnits.value
|
|
|
+ const arr: GridItem[] = []
|
|
|
+ for (let i = 0; i <= count + extraGridCount * 2; i++) {
|
|
|
+ const numIndex = i - extraGridCount
|
|
|
+ const num = props.min + numIndex * props.step
|
|
|
+ const displayNum = num
|
|
|
+ const isLongGrid = (numIndex % 10 === 0)
|
|
|
+ const showText = isLongGrid && num >= props.min && num <= props.max
|
|
|
+ arr.push({ num, displayNum, isLongGrid, showText })
|
|
|
+ }
|
|
|
+ gridList.value = arr
|
|
|
+ totalWidth.value = arr.length * props.gutter
|
|
|
+}
|
|
|
+
|
|
|
+buildGrid()
|
|
|
+
|
|
|
+// offsetScroll: amount to offset due to extra grids on left
|
|
|
+const offsetScroll = computed(() => extraGridCount * props.gutter)
|
|
|
+
|
|
|
+// initial position
|
|
|
+onMounted(() => {
|
|
|
+ // center on initial value
|
|
|
+ const initIndex = Math.round((props.initialValue - props.min) / props.step)
|
|
|
+ scrollLeft.value = initIndex * props.gutter + offsetScroll.value - (uni?.getSystemInfoSync?.().windowWidth || 375) / 2 + props.gutter / 2
|
|
|
+})
|
|
|
+
|
|
|
+function onScroll(e: any) {
|
|
|
+ const left = e?.detail?.scrollLeft ?? e?.detail?.scrollLeft ?? 0
|
|
|
+ actualScrollLeft.value = left
|
|
|
+ let value = Math.round((left - offsetScroll.value) / props.gutter) * props.step + props.min
|
|
|
+ if (value < props.min) value = props.min
|
|
|
+ if (value > props.max) value = props.max
|
|
|
+ emit('update:value', value)
|
|
|
+}
|
|
|
+
|
|
|
+function adjustScrollPosition() {
|
|
|
+ const left = actualScrollLeft.value
|
|
|
+ const minLeft = offsetScroll.value
|
|
|
+ const maxLeft = (totalUnits.value) * props.gutter + offsetScroll.value
|
|
|
+ if (left < minLeft) {
|
|
|
+ scrollLeft.value = minLeft
|
|
|
+ } else if (left > maxLeft) {
|
|
|
+ scrollLeft.value = maxLeft
|
|
|
+ } else {
|
|
|
+ // snap to nearest gutter
|
|
|
+ const dry = left - offsetScroll.value
|
|
|
+ const rem = Math.round(dry / props.gutter) * props.gutter
|
|
|
+ scrollLeft.value = rem + offsetScroll.value
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onTouchEnd() {
|
|
|
+ // adjust position on touch end
|
|
|
+ adjustScrollPosition()
|
|
|
+ // compute value and emit change
|
|
|
+ const left = scrollLeft.value
|
|
|
+ let value = Math.round((left - offsetScroll.value) / props.gutter) * props.step + props.min
|
|
|
+ if (value < props.min) value = props.min
|
|
|
+ if (value > props.max) value = props.max
|
|
|
+ emit('change', value)
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.ruler-wrapper { position: relative; height: 80rpx; display:flex; align-items:center }
|
|
|
+.ruler-scroll { width: 100%; height: 80rpx }
|
|
|
+.ruler-inner { display:flex; align-items:flex-end }
|
|
|
+.grid-item { width: 10px; height: 24px; display:flex; align-items:flex-end; justify-content:center }
|
|
|
+.grid-item.long { height: 40px }
|
|
|
+.grid-num { font-size: 24rpx; color: #666 }
|
|
|
+.indicator { position: absolute; left: 50%; top: 0; transform: translateX(-50%); width: 2px; height: 100%; background: #ff6a00 }
|
|
|
+</style>
|