Pārlūkot izejas kodu

feat(health): 添加身高刻度尺组件并实现联动功能

- 引入并使用 ScaleRuler 组件用于身高选择
- 实现刻度尺值变化的处理逻辑
- 新增 onRulerUpdate 和 onRulerChange 方法同步数据
- 刻度尺选中值自动填充至添加输入框
- 设置刻度尺默认值为 170cm,范围 120-220cm
mcbaiyun 2 mēneši atpakaļ
vecāks
revīzija
f766aea8c2
2 mainītis faili ar 151 papildinājumiem un 1 dzēšanām
  1. 135 0
      src/components/scale-ruler.vue
  2. 16 1
      src/pages/health/height.vue

+ 135 - 0
src/components/scale-ruler.vue

@@ -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>

+ 16 - 1
src/pages/health/height.vue

@@ -16,7 +16,10 @@
   <canvas ref="chartCanvas" id="chartCanvas" canvas-id="heightChart" class="chart-canvas" width="340" height="120"></canvas>
   <canvas ref="chartCanvas" id="chartCanvas" canvas-id="heightChart" class="chart-canvas" width="340" height="120"></canvas>
     </view>
     </view>
 
 
-    <view class="content">
+  <!-- 刻度尺控制(示例) -->
+  <ScaleRuler :min="120" :max="220" :step="1" :gutter="8" :initialValue="170" @change="onRulerChange" @update:value="onRulerUpdate" />
+
+  <view class="content">
       <view class="summary">共 {{ records.length }} 条记录,本月平均:{{ averageHeight }} cm</view>
       <view class="summary">共 {{ records.length }} 条记录,本月平均:{{ averageHeight }} cm</view>
 
 
       <view class="list">
       <view class="list">
@@ -62,6 +65,7 @@
 import { ref, computed } from 'vue'
 import { ref, computed } from 'vue'
 import CustomNav from '@/components/CustomNav.vue'
 import CustomNav from '@/components/CustomNav.vue'
 import TabBar from '@/components/TabBar.vue'
 import TabBar from '@/components/TabBar.vue'
+import ScaleRuler from '@/components/scale-ruler.vue'
 
 
 type RecordItem = { id: string; date: string; height: number }
 type RecordItem = { id: string; date: string; height: number }
 
 
@@ -455,6 +459,17 @@ const addDate = ref(formatPickerDate(new Date()))
 const addDateLabel = ref(formatDisplayDate(new Date()))
 const addDateLabel = ref(formatDisplayDate(new Date()))
 const addHeight = ref<number | null>(null)
 const addHeight = ref<number | null>(null)
 
 
+// 刻度尺联动值
+const rulerValue = ref<number>(170)
+function onRulerUpdate(val: number) {
+  rulerValue.value = val
+}
+function onRulerChange(val: number) {
+  rulerValue.value = val
+  // 额外行为可在此扩展,例如把值填入添加输入框
+  addHeight.value = val
+}
+
 function openAdd() {
 function openAdd() {
   showAdd.value = true
   showAdd.value = true
 }
 }