|
|
@@ -5,14 +5,11 @@
|
|
|
<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="{ width: props.gutter + 'px' }">
|
|
|
- <!-- 刻度线(位于上方) -->
|
|
|
<view class="tick" :class="{ long: g.isLongGrid }"></view>
|
|
|
- <!-- 刻度文字放在刻度线下面 -->
|
|
|
<text v-if="g.showText" class="grid-num">{{ g.displayNum }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</scroll-view>
|
|
|
- <!-- 中心指示器:上方显示当前值的方框,下方显示指向刻度的三角形 -->
|
|
|
<view class="indicator">
|
|
|
<view class="value-box"><text class="value-text">{{ currentValue }}</text></view>
|
|
|
<view class="indicator-triangle"></view>
|
|
|
@@ -31,10 +28,10 @@ interface GridItem { num: number; displayNum: number | string; isLongGrid: boole
|
|
|
const props = defineProps({
|
|
|
min: { type: Number, default: 0 },
|
|
|
max: { type: Number, default: 100 },
|
|
|
- step: { type: Number, default: 1 }, // value step
|
|
|
+ step: { type: Number, default: 1 },
|
|
|
unit: { type: String, default: '' },
|
|
|
initialValue: { type: Number, default: 0 },
|
|
|
- gutter: { type: Number, default: 10 }, // px per unit
|
|
|
+ gutter: { type: Number, default: 10 },
|
|
|
visible: { type: Boolean, default: true },
|
|
|
useAnimation: { type: Boolean, default: true }
|
|
|
})
|
|
|
@@ -44,13 +41,13 @@ const emit = defineEmits(['update:value', 'change'])
|
|
|
const instance = getCurrentInstance()
|
|
|
const scrollLeft = ref(0)
|
|
|
const actualScrollLeft = ref(0)
|
|
|
-// 当前中心值(整数)
|
|
|
+
|
|
|
const currentValue = ref(Math.round(props.initialValue || props.min))
|
|
|
|
|
|
-// 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[]>([])
|
|
|
@@ -61,8 +58,8 @@ const gridItemStyle = computed(() => ({ width: props.gutter + 'px', height: '24p
|
|
|
function buildGrid() {
|
|
|
const count = totalUnits.value
|
|
|
const arr: GridItem[] = []
|
|
|
- for (let i = 0; i <= count + extraGridCount * 2; i++) {
|
|
|
- const numIndex = i - extraGridCount
|
|
|
+ for (let i = 0; i <= count + extraGridCount * 1; i++) {
|
|
|
+ const numIndex = i - extraGridCount /2
|
|
|
const num = props.min + numIndex * props.step
|
|
|
const displayNum = num
|
|
|
const isLongGrid = (numIndex % 10 === 0)
|
|
|
@@ -75,59 +72,57 @@ function buildGrid() {
|
|
|
|
|
|
buildGrid()
|
|
|
|
|
|
-// offsetScroll: amount to offset due to extra grids on left
|
|
|
-const offsetScroll = computed(() => extraGridCount * props.gutter)
|
|
|
+const offsetScroll = computed(() => extraGridCount * props.gutter/2)
|
|
|
|
|
|
-// window width and half (indicator is centered horizontally)
|
|
|
const windowWidth = uni?.getSystemInfoSync?.().windowWidth || 375
|
|
|
const halfWindow = windowWidth / 2
|
|
|
|
|
|
-// initial position
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
- // center on initial value (align cell center under indicator)
|
|
|
+
|
|
|
const initIndex = Math.round((props.initialValue - props.min) / props.step)
|
|
|
- // place the center of the initIndex cell under the center indicator
|
|
|
+
|
|
|
scrollLeft.value = offsetScroll.value + initIndex * props.gutter + props.gutter / 2 - halfWindow
|
|
|
- // 初始化当前显示值
|
|
|
+
|
|
|
currentValue.value = Math.round(initIndex * props.step + props.min)
|
|
|
})
|
|
|
|
|
|
function onScroll(e: any) {
|
|
|
const left = e?.detail?.scrollLeft ?? e?.detail?.scrollLeft ?? 0
|
|
|
actualScrollLeft.value = left
|
|
|
- // compute number index (units from min) under the centered indicator (based on cell center)
|
|
|
+
|
|
|
const numIndex = Math.round((left + halfWindow - offsetScroll.value - props.gutter / 2) / props.gutter)
|
|
|
let value = numIndex * props.step + props.min
|
|
|
if (value < props.min) value = props.min
|
|
|
if (value > props.max) value = props.max
|
|
|
- // 更新当前显示值(滚动中实时更新)
|
|
|
+
|
|
|
currentValue.value = Math.round(value)
|
|
|
emit('update:value', value)
|
|
|
}
|
|
|
|
|
|
function adjustScrollPosition() {
|
|
|
- // snap so the nearest tick is centered under the indicator
|
|
|
+
|
|
|
const left = actualScrollLeft.value
|
|
|
- // compute number index under center
|
|
|
+
|
|
|
const numIndex = Math.round((left + halfWindow - offsetScroll.value - props.gutter / 2) / props.gutter)
|
|
|
- // compute target scrollLeft that centers this index's center under the indicator
|
|
|
+
|
|
|
const targetLeft = offsetScroll.value + numIndex * props.gutter + props.gutter / 2 - halfWindow
|
|
|
- // clamp to valid scroll range
|
|
|
+
|
|
|
const maxScroll = Math.max(0, totalWidth.value - windowWidth)
|
|
|
const clamped = Math.min(Math.max(targetLeft, 0), maxScroll)
|
|
|
scrollLeft.value = clamped
|
|
|
}
|
|
|
|
|
|
function onTouchEnd() {
|
|
|
- // adjust position on touch end
|
|
|
+
|
|
|
adjustScrollPosition()
|
|
|
- // compute value and emit change
|
|
|
+
|
|
|
const left = scrollLeft.value
|
|
|
const numIndex = Math.round((left + halfWindow - offsetScroll.value - props.gutter / 2) / props.gutter)
|
|
|
let value = numIndex * props.step + props.min
|
|
|
if (value < props.min) value = props.min
|
|
|
if (value > props.max) value = props.max
|
|
|
- // 更新当前显示值(吸附后确定值)
|
|
|
+
|
|
|
currentValue.value = Math.round(value)
|
|
|
emit('change', value)
|
|
|
}
|
|
|
@@ -136,7 +131,6 @@ function onTouchEnd() {
|
|
|
<style scoped>
|
|
|
.ruler-wrapper {
|
|
|
position: relative;
|
|
|
- /* 增高以给刻度线下面留白 */
|
|
|
height: 260rpx;
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
@@ -150,23 +144,20 @@ function onTouchEnd() {
|
|
|
|
|
|
.ruler-inner {
|
|
|
display: flex;
|
|
|
- /* 让刻度线靠上排列,文字在下方 */
|
|
|
align-items: flex-start;
|
|
|
}
|
|
|
|
|
|
.grid-item {
|
|
|
width: 10px;
|
|
|
- /* 不固定高度,让内部内容控制布局 */
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: flex-start;
|
|
|
padding-top: 8rpx;
|
|
|
- /* 给刻度线一点顶部间距 */
|
|
|
}
|
|
|
|
|
|
|
|
|
-/* 刻度线样式,短刻度和长刻度高度不同 */
|
|
|
+
|
|
|
.tick {
|
|
|
width: 2px;
|
|
|
height: 40rpx;
|
|
|
@@ -184,7 +175,7 @@ function onTouchEnd() {
|
|
|
font-size: 28rpx;
|
|
|
color: #666;
|
|
|
margin-top: 8rpx;
|
|
|
- /* 刻度线下面的间距 */
|
|
|
+
|
|
|
}
|
|
|
|
|
|
.indicator {
|
|
|
@@ -205,7 +196,6 @@ function onTouchEnd() {
|
|
|
margin-top: -4px;
|
|
|
margin-left: 1px;
|
|
|
height: 15px;
|
|
|
- /* 可视为指示器从 value box 底部到 triangle 顶部的线长,可调整 */
|
|
|
background: #ff6a00;
|
|
|
}
|
|
|
|
|
|
@@ -215,17 +205,14 @@ function onTouchEnd() {
|
|
|
border-left: 8px solid transparent;
|
|
|
border-right: 8px solid transparent;
|
|
|
border-top: 10px solid #ff6a00;
|
|
|
- /* 向上的三角形 */
|
|
|
margin-top: 60px;
|
|
|
}
|
|
|
|
|
|
.value-box {
|
|
|
margin-top: 20rpx;
|
|
|
position: absolute;
|
|
|
- /* 将方框放在指示线之上,距离可根据值框高度调整 */
|
|
|
left: 50%;
|
|
|
transform: translateX(-50%);
|
|
|
- /* background: #ff6a00; */
|
|
|
padding: 6rpx 12rpx;
|
|
|
border-radius: 6rpx;
|
|
|
display: flex;
|
|
|
@@ -237,9 +224,6 @@ function onTouchEnd() {
|
|
|
color: #000000;
|
|
|
font-size: 28px;
|
|
|
font-weight: 900;
|
|
|
- /* 使用 24px 显示当前数值 */
|
|
|
line-height: 1;
|
|
|
}
|
|
|
-
|
|
|
-
|
|
|
</style>
|