scale-ruler.vue 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <template>
  2. <view class="ruler-wrapper" v-show="visible">
  3. <scroll-view class="ruler-scroll" scroll-x :scroll-left="scrollLeft" :scroll-with-animation="useAnimation"
  4. @scroll="onScroll" @touchend="onTouchEnd" @touchcancel="onTouchEnd" scroll-into-view="">
  5. <view class="ruler-inner" :style="{ width: totalWidth + 'px' }">
  6. <view v-for="(g, idx) in gridList" :key="idx" class="grid-item" :class="{ long: g.isLongGrid }"
  7. :style="{ width: props.gutter + 'px' }">
  8. <view class="tick" :class="{ long: g.isLongGrid }"></view>
  9. <text v-if="g.showText" class="grid-num">{{ g.displayNum }}</text>
  10. </view>
  11. </view>
  12. </scroll-view>
  13. <view class="indicator">
  14. <view class="value-box"><text class="value-text">{{ currentValue }}</text></view>
  15. <view class="indicator-triangle"></view>
  16. <view class="indicator-line"></view>
  17. </view>
  18. </view>
  19. </template>
  20. <script setup lang="ts">
  21. import { ref, computed, watch, onMounted } from 'vue'
  22. import { getCurrentInstance } from 'vue'
  23. interface GridItem { num: number; displayNum: number | string; isLongGrid: boolean; showText: boolean }
  24. const props = defineProps({
  25. min: { type: Number, default: 0 },
  26. max: { type: Number, default: 100 },
  27. step: { type: Number, default: 1 },
  28. unit: { type: String, default: '' },
  29. initialValue: { type: Number, default: 0 },
  30. gutter: { type: Number, default: 10 },
  31. visible: { type: Boolean, default: true },
  32. useAnimation: { type: Boolean, default: true }
  33. })
  34. const emit = defineEmits(['update:value', 'change'])
  35. const instance = getCurrentInstance()
  36. const scrollLeft = ref(0)
  37. const actualScrollLeft = ref(0)
  38. const currentValue = ref(Math.round(props.initialValue || props.min))
  39. const totalUnits = computed(() => Math.round((props.max - props.min) / props.step))
  40. const extraGridCount = Math.ceil((uni?.getSystemInfoSync?.().windowWidth || 375) / props.gutter / 2) * 2
  41. const gridList = ref<GridItem[]>([])
  42. const totalWidth = ref(0)
  43. const gridItemStyle = computed(() => ({ width: props.gutter + 'px', height: '24px' }))
  44. function buildGrid() {
  45. const count = totalUnits.value
  46. const arr: GridItem[] = []
  47. for (let i = 0; i <= count + extraGridCount * 1; i++) {
  48. const numIndex = i - extraGridCount / 2
  49. const num = props.min + numIndex * props.step
  50. const displayNum = num
  51. const isLongGrid = (numIndex % 10 === 0)
  52. const showText = isLongGrid && num >= props.min && num <= props.max
  53. arr.push({ num, displayNum, isLongGrid, showText })
  54. }
  55. gridList.value = arr
  56. totalWidth.value = arr.length * props.gutter
  57. }
  58. buildGrid()
  59. const offsetScroll = computed(() => extraGridCount * props.gutter/2)
  60. const windowWidth = uni?.getSystemInfoSync?.().windowWidth || 375
  61. const halfWindow = windowWidth / 2
  62. onMounted(() => {
  63. const initIndex = Math.round((props.initialValue - props.min) / props.step)
  64. scrollLeft.value = offsetScroll.value + initIndex * props.gutter + props.gutter / 2 - halfWindow
  65. currentValue.value = Math.round(initIndex * props.step + props.min)
  66. })
  67. function onScroll(e: any) {
  68. const left = e?.detail?.scrollLeft ?? e?.detail?.scrollLeft ?? 0
  69. actualScrollLeft.value = left
  70. const numIndex = Math.round((left + halfWindow - offsetScroll.value - props.gutter / 2) / props.gutter)
  71. let value = numIndex * props.step + props.min
  72. if (value < props.min) value = props.min
  73. if (value > props.max) value = props.max
  74. currentValue.value = Math.round(value)
  75. emit('update:value', value)
  76. }
  77. function adjustScrollPosition() {
  78. const left = actualScrollLeft.value
  79. const numIndex = Math.round((left + halfWindow - offsetScroll.value - props.gutter / 2) / props.gutter)
  80. const targetLeft = offsetScroll.value + numIndex * props.gutter + props.gutter / 2 - halfWindow
  81. const maxScroll = Math.max(0, totalWidth.value - windowWidth)
  82. const clamped = Math.min(Math.max(targetLeft, 0), maxScroll)
  83. scrollLeft.value = clamped
  84. }
  85. function onTouchEnd() {
  86. adjustScrollPosition()
  87. const left = scrollLeft.value
  88. const numIndex = Math.round((left + halfWindow - offsetScroll.value - props.gutter / 2) / props.gutter)
  89. let value = numIndex * props.step + props.min
  90. if (value < props.min) value = props.min
  91. if (value > props.max) value = props.max
  92. currentValue.value = Math.round(value)
  93. emit('change', value)
  94. }
  95. </script>
  96. <style scoped>
  97. .ruler-wrapper {
  98. position: relative;
  99. height: 520rpx;
  100. display: flex;
  101. align-items: flex-start;
  102. }
  103. .ruler-scroll {
  104. margin-top: 210rpx;
  105. width: 100%;
  106. height: 180rpx;
  107. overflow-x: scroll;
  108. scrollbar-width: none; /* Firefox */
  109. -ms-overflow-style: none; /* IE and Edge */
  110. }
  111. .ruler-scroll::-webkit-scrollbar {
  112. display: none; /* Chrome, Safari, Opera */
  113. }
  114. .ruler-inner {
  115. display: flex;
  116. align-items: flex-start;
  117. }
  118. .grid-item {
  119. width: 10px;
  120. display: flex;
  121. flex-direction: column;
  122. align-items: center;
  123. justify-content: flex-start;
  124. padding-top: 8rpx;
  125. }
  126. .tick {
  127. width: 2px;
  128. margin-top: 10rpx;
  129. height: 60rpx;
  130. background: #d5d5d5;
  131. }
  132. .tick.long {
  133. width: 3px;
  134. margin-top: 0rpx;
  135. /* margin-left: 1px; */
  136. height: 80rpx;
  137. background: #414141;
  138. }
  139. .grid-num {
  140. font-size: 36rpx;
  141. color: #666;
  142. margin-block: 8rpx;
  143. }
  144. .indicator {
  145. position: absolute;
  146. left: 50%;
  147. top: 0;
  148. transform: translateX(-50%);
  149. width: 2px;
  150. height: 100%;
  151. display: flex;
  152. align-items: center;
  153. flex-direction: column;
  154. pointer-events: none;
  155. }
  156. .indicator-line {
  157. width: 4px;
  158. margin-top: -4px;
  159. /* margin-left: 1px; */
  160. height: 15px;
  161. background: #ff6a00;
  162. }
  163. .indicator-triangle {
  164. width: 0;
  165. height: 0;
  166. border-left: 8px solid transparent;
  167. border-right: 8px solid transparent;
  168. border-top: 10px solid #ff6a00;
  169. margin-top: 100px;
  170. }
  171. .value-box {
  172. margin-top: 70rpx;
  173. position: absolute;
  174. left: 50%;
  175. transform: translateX(-50%);
  176. padding: 6rpx 12rpx;
  177. border-radius: 6rpx;
  178. display: flex;
  179. align-items: center;
  180. justify-content: center;
  181. }
  182. .value-text {
  183. color: #000000;
  184. font-size: 70rpx;
  185. font-weight: 900;
  186. line-height: 1;
  187. }
  188. </style>