|
@@ -1,6 +1,5 @@
|
|
|
<template>
|
|
<template>
|
|
|
<view class="ruler-wrapper" v-show="visible">
|
|
<view class="ruler-wrapper" v-show="visible">
|
|
|
- <view class="value-bubble" v-if="currentValue !== null">{{ currentValue }}{{ props.unit }}</view>
|
|
|
|
|
<scroll-view
|
|
<scroll-view
|
|
|
class="ruler-scroll"
|
|
class="ruler-scroll"
|
|
|
scroll-x
|
|
scroll-x
|
|
@@ -16,17 +15,14 @@
|
|
|
v-for="(g, idx) in gridList"
|
|
v-for="(g, idx) in gridList"
|
|
|
:key="idx"
|
|
:key="idx"
|
|
|
class="grid-item"
|
|
class="grid-item"
|
|
|
- :class="{ long: g.isLongGrid, labeled: g.showText }"
|
|
|
|
|
- :style="{ width: props.gutter + 'px' }"
|
|
|
|
|
|
|
+ :class="{ long: g.isLongGrid }"
|
|
|
|
|
+ :style="{ width: props.gutter + 'px', height: g.showText ? '40px' : '24px' }"
|
|
|
>
|
|
>
|
|
|
- <view class="tick" :class="{ long: g.isLongGrid }"></view>
|
|
|
|
|
<text v-if="g.showText" class="grid-num">{{ g.displayNum }}</text>
|
|
<text v-if="g.showText" class="grid-num">{{ g.displayNum }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</scroll-view>
|
|
</scroll-view>
|
|
|
- <view class="indicator">
|
|
|
|
|
- <view class="indicator-line"></view>
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <view class="indicator"></view>
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -52,7 +48,6 @@ const emit = defineEmits(['update:value', 'change'])
|
|
|
const instance = getCurrentInstance()
|
|
const instance = getCurrentInstance()
|
|
|
const scrollLeft = ref(0)
|
|
const scrollLeft = ref(0)
|
|
|
const actualScrollLeft = ref(0)
|
|
const actualScrollLeft = ref(0)
|
|
|
-const currentValue = ref<number | null>(null)
|
|
|
|
|
|
|
|
|
|
// prepare grid
|
|
// prepare grid
|
|
|
const totalUnits = computed(() => Math.round((props.max - props.min) / props.step))
|
|
const totalUnits = computed(() => Math.round((props.max - props.min) / props.step))
|
|
@@ -89,47 +84,21 @@ const offsetScroll = computed(() => extraGridCount * props.gutter)
|
|
|
const windowWidth = uni?.getSystemInfoSync?.().windowWidth || 375
|
|
const windowWidth = uni?.getSystemInfoSync?.().windowWidth || 375
|
|
|
const halfWindow = windowWidth / 2
|
|
const halfWindow = windowWidth / 2
|
|
|
|
|
|
|
|
-// centered positions for extremes
|
|
|
|
|
-const minCenterLeft = computed(() => offsetScroll.value - halfWindow + props.gutter / 2)
|
|
|
|
|
-const maxCenterLeft = computed(() => totalUnits.value * props.gutter + offsetScroll.value - halfWindow + props.gutter / 2)
|
|
|
|
|
-
|
|
|
|
|
// initial position
|
|
// initial position
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
// center on initial value
|
|
// center on initial value
|
|
|
const initIndex = Math.round((props.initialValue - props.min) / props.step)
|
|
const initIndex = Math.round((props.initialValue - props.min) / props.step)
|
|
|
- // use computed windowWidth to center the initial index under the indicator
|
|
|
|
|
- scrollLeft.value = initIndex * props.gutter + offsetScroll.value - halfWindow + props.gutter / 2
|
|
|
|
|
|
|
+ scrollLeft.value = initIndex * props.gutter + offsetScroll.value - (uni?.getSystemInfoSync?.().windowWidth || 375) / 2 + props.gutter / 2
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
function onScroll(e: any) {
|
|
function onScroll(e: any) {
|
|
|
const left = e?.detail?.scrollLeft ?? e?.detail?.scrollLeft ?? 0
|
|
const left = e?.detail?.scrollLeft ?? e?.detail?.scrollLeft ?? 0
|
|
|
actualScrollLeft.value = left
|
|
actualScrollLeft.value = left
|
|
|
- // compute index under the centered indicator (float)
|
|
|
|
|
- const centerPos = left + halfWindow - offsetScroll.value - props.gutter / 2
|
|
|
|
|
- const idxFloat = centerPos / props.gutter
|
|
|
|
|
- // calculate actual scrollable max
|
|
|
|
|
- const maxScroll = Math.max(0, totalWidth.value - windowWidth)
|
|
|
|
|
- // if center would go left of min, clamp scrollLeft to minCenterLeft and emit min
|
|
|
|
|
- if (idxFloat <= 0) {
|
|
|
|
|
- const clamped = Math.min(Math.max(minCenterLeft.value, 0), maxScroll)
|
|
|
|
|
- scrollLeft.value = clamped
|
|
|
|
|
- actualScrollLeft.value = clamped
|
|
|
|
|
- emit('update:value', props.min)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- // if center would go right of max, clamp to maxCenterLeft and emit max
|
|
|
|
|
- if (idxFloat >= totalUnits.value) {
|
|
|
|
|
- const clamped = Math.min(Math.max(maxCenterLeft.value, 0), maxScroll)
|
|
|
|
|
- scrollLeft.value = clamped
|
|
|
|
|
- actualScrollLeft.value = clamped
|
|
|
|
|
- emit('update:value', props.max)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- const idx = Math.round(idxFloat)
|
|
|
|
|
|
|
+ // compute index under the centered indicator
|
|
|
|
|
+ const idx = Math.round((left + halfWindow - offsetScroll.value - props.gutter / 2) / props.gutter)
|
|
|
let value = idx * props.step + props.min
|
|
let value = idx * props.step + props.min
|
|
|
if (value < props.min) value = props.min
|
|
if (value < props.min) value = props.min
|
|
|
if (value > props.max) value = props.max
|
|
if (value > props.max) value = props.max
|
|
|
- currentValue.value = value
|
|
|
|
|
emit('update:value', value)
|
|
emit('update:value', value)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -140,14 +109,9 @@ function adjustScrollPosition() {
|
|
|
const idx = Math.round((left + halfWindow - offsetScroll.value - props.gutter / 2) / props.gutter)
|
|
const idx = Math.round((left + halfWindow - offsetScroll.value - props.gutter / 2) / props.gutter)
|
|
|
// compute target scrollLeft that centers this index
|
|
// compute target scrollLeft that centers this index
|
|
|
const targetLeft = idx * props.gutter + offsetScroll.value - halfWindow + props.gutter / 2
|
|
const targetLeft = idx * props.gutter + offsetScroll.value - halfWindow + props.gutter / 2
|
|
|
- // compute centered positions for min and max so extremes map to centered
|
|
|
|
|
- const minCenterLeft = 0 + offsetScroll.value - halfWindow + props.gutter / 2
|
|
|
|
|
- const maxCenterLeft = totalUnits.value * props.gutter + offsetScroll.value - halfWindow + props.gutter / 2
|
|
|
|
|
- // actual scrollable bounds
|
|
|
|
|
|
|
+ // clamp to valid scroll range
|
|
|
const maxScroll = Math.max(0, totalWidth.value - windowWidth)
|
|
const maxScroll = Math.max(0, totalWidth.value - windowWidth)
|
|
|
- // clamp target to the centered min/max first, then to actual scroll range
|
|
|
|
|
- const bounded = Math.min(Math.max(targetLeft, minCenterLeft), maxCenterLeft)
|
|
|
|
|
- const clamped = Math.min(Math.max(bounded, 0), maxScroll)
|
|
|
|
|
|
|
+ const clamped = Math.min(Math.max(targetLeft, 0), maxScroll)
|
|
|
scrollLeft.value = clamped
|
|
scrollLeft.value = clamped
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -160,22 +124,16 @@ function onTouchEnd() {
|
|
|
let value = idx * props.step + props.min
|
|
let value = idx * props.step + props.min
|
|
|
if (value < props.min) value = props.min
|
|
if (value < props.min) value = props.min
|
|
|
if (value > props.max) value = props.max
|
|
if (value > props.max) value = props.max
|
|
|
- currentValue.value = value
|
|
|
|
|
emit('change', value)
|
|
emit('change', value)
|
|
|
}
|
|
}
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
-.ruler-wrapper { position: relative; height: 90rpx; display:flex; align-items:center; padding-top:20rpx }
|
|
|
|
|
-.value-bubble { position: absolute; left: 50%; top: 0; transform: translateX(-50%); background: #ff6a00; color: #fff; padding:6rpx 14rpx; border-radius: 18rpx; font-weight:700; box-shadow: 0 6rpx 18rpx rgba(255,106,0,0.18); z-index: 20 }
|
|
|
|
|
-.ruler-scroll { width: 100%; height: 90rpx; overflow: hidden }
|
|
|
|
|
-.ruler-inner { display:flex; align-items:flex-end; padding-bottom: 8rpx }
|
|
|
|
|
-.grid-item { display:flex; flex-direction:column; align-items:center; justify-content:flex-end; height: 60px; position: relative }
|
|
|
|
|
-.grid-item.labeled { height: 80px }
|
|
|
|
|
-.tick { width: 2px; background: #ccc; transition: background-color .12s, height .12s }
|
|
|
|
|
-.tick.long { height: 34px; background: #999 }
|
|
|
|
|
-.grid-item.labeled .grid-num { margin-top:6rpx; color:#333; font-size:24rpx }
|
|
|
|
|
-.grid-num { font-size: 20rpx; color: #666 }
|
|
|
|
|
-.indicator { position: absolute; left: 50%; top: 18rpx; transform: translateX(-50%); width: 36rpx; height: 54rpx; display:flex; align-items:center; justify-content:center; pointer-events:none }
|
|
|
|
|
-.indicator-line { width: 2px; height: 100%; background: #ff6a00; box-shadow: 0 2px 6px rgba(255,106,0,0.25); border-radius:1px }
|
|
|
|
|
|
|
+.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>
|
|
</style>
|