Преглед изворни кода

feat(patient): 整合身高体重记录到体测页面

- 删除独立的身高、体重、BMI记录页面
- 新增体测记录页面,统一管理身高、体重和BMI数据
- 支持在体测页面切换查看不同指标的趋势图
- 添加体测数据时可同时输入身高和体重
- 根据BMI值为记录项添加颜色标识
- 更新页面路由配置,移除旧页面路径,新增体测记录路径
mcbaiyun пре 1 месец
родитељ
комит
90787e8fb6

+ 6 - 18
src/pages.json

@@ -36,24 +36,6 @@
 				"navigationBarTitleText": "患者健康数据"
 			}
 		},
-		{
-			"path": "pages/patient/health/details/height",
-			"style": {
-				"navigationBarTitleText": "身高记录"
-			}
-		},
-		{
-			"path": "pages/patient/health/details/weight",
-			"style": {
-				"navigationBarTitleText": "体重记录"
-			}
-		},
-		{
-			"path": "pages/patient/health/details/bmi",
-			"style": {
-				"navigationBarTitleText": "BMI记录"
-			}
-		},
 		{
 			"path": "pages/patient/health/details/blood-pressure",
 			"style": {
@@ -143,6 +125,12 @@
 			"style": {
 				"navigationBarTitleText": "二维码"
 			}
+		},
+		{
+			"path": "pages/patient/health/details/physical",
+			"style": {
+				"navigationBarTitleText": "体测记录"
+			}
 		}
 	],
 	"globalStyle": {

+ 0 - 18
src/pages/patient/health/details/bmi.vue

@@ -1,18 +0,0 @@
-<template>
-  <CustomNav title="BMI" leftType="back" />
-  <view class="content">
-    <view class="placeholder">自动根据身高体重计算并展示 BMI 历史</view>
-  </view>
-
-</template>
-
-<script setup lang="ts">
-import CustomNav from '@/components/custom-nav.vue'
-
-
-</script>
-
-<style>
-.placeholder { padding: 40rpx; color: #666 }
-.content { padding-top: calc(var(--status-bar-height) + 44px) }
-</style>

+ 0 - 663
src/pages/patient/health/details/height.vue

@@ -1,663 +0,0 @@
-<template>
-  <CustomNav title="身高" leftType="back" />
-  <view class="page">
-    <view class="header">
-      <view class="month-selector">
-        <button class="btn" @click="prevPeriod">‹</button>
-        <view class="period-controls">
-          <picker mode="multiSelector" :value="pickerValue" :range="pickerRange" @change="onPickerChange">
-            <view class="month-label">{{ displayPeriod }}</view>
-          </picker>
-          <view class="view-toggle">
-            <button :class="['toggle-btn', { active: viewMode === 'month' }]" @click="setViewMode('month')">月</button>
-            <button :class="['toggle-btn', { active: viewMode === 'week' }]" @click="setViewMode('week')">周</button>
-          </view>
-        </view>
-        <button class="btn" @click="nextPeriod">›</button>
-      </view>
-    </view>
-
-    <!-- 趋势图 - 简化canvas设置 -->
-    <view class="chart-wrap">
-      <view class="chart-header">{{ viewMode === 'month' ? '本月' : '本周' }}趋势</view>
-      <canvas 
-        canvas-id="heightChart" 
-        id="heightChart" 
-        class="chart-canvas"
-        :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
-      ></canvas>
-    </view>
-
-    <view class="content">
-      <view class="summary">共 {{ records.length }} 条记录{{ viewMode === 'month' ? ',本月' : ',本周' }}平均:{{ averageHeight }} cm</view>
-
-      <view class="list">
-        <view v-if="records.length === 0" class="empty">{{ viewMode === 'month' ? '本月' : '本周' }}暂无身高记录,点击右下角 + 添加</view>
-        <view v-for="(r, idx) in records" :key="r.id" class="list-item">
-          <view class="date">{{ r.date }}</view>
-          <view class="value">{{ r.height }} cm</view>
-          <button class="btn-delete" @click.stop.prevent="confirmDeleteRecord(r.id)">✕</button>
-        </view>
-      </view>
-    </view>
-
-    <!-- 悬浮添加按钮 -->
-    <view class="fab" @click="openAdd">
-      <view class="fab-inner">+</view>
-    </view>
-
-    <!-- 添加模态(包含刻度尺) -->
-    <view class="modal" v-if="showAdd">
-      <view class="modal-backdrop" @click="closeAdd"></view>
-      <view class="modal-panel">
-        <view class="drag-handle"></view>
-        <view class="modal-header"><text class="modal-title">添加身高</text></view>
-
-        <view class="modal-inner">
-          <view class="form-row">
-            <text class="label">日期</text>
-            <picker mode="date" :value="addDate" @change="onAddDateChange">
-              <view class="picker-display">{{ addDateLabel }}</view>
-            </picker>
-          </view>
-
-          <view class="form-row">
-            <text class="label">身高 (cm)</text>
-            <input type="number" v-model.number="addHeight" class="input" placeholder="请输入身高" />
-          </view>
-        </view>
-
-        <view class="ruler-wrap">
-          <ScaleRuler v-if="showAdd" :min="120" :max="220" :step="1" :gutter="16" :initialValue="addHeight ?? 170" @update="onAddRulerUpdate" @change="onAddRulerChange" />
-        </view>
-
-        <view class="fixed-footer">
-          <button class="btn-primary btn-full" @click="confirmAdd">保存</button>
-        </view>
-      </view>
-    </view>
-
-  </view>
-
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted, watch, nextTick, onBeforeUnmount, getCurrentInstance } from 'vue'
-import { createUChart } from '@/composables/useUChart'
-import CustomNav from '@/components/custom-nav.vue'
-
-import ScaleRuler from '@/components/scale-ruler.vue'
-import { getWeekStart, getWeekEnd, formatDisplayDate, formatPickerDate, daysInMonth } from '@/utils/date'
-
-type RecordItem = { id: string; date: string; height: number }
-
-// 当前展示年月
-const current = ref(new Date())
-const pickerValue = ref([current.value.getFullYear() - 2000, current.value.getMonth()]) // 年从2000年开始,月0-11
-
-// 视图模式:'month' 或 'week'
-const viewMode = ref<'month' | 'week'>('month')
-
-// 年月选择器的选项范围
-const pickerRange = ref([
-  Array.from({ length: 50 }, (_, i) => `${2000 + i}年`), // 2000-2049年
-  Array.from({ length: 12 }, (_, i) => `${i + 1}月`) // 1-12月
-])
-
-// 明确的canvas尺寸(将由 getCanvasSize 初始化以匹配设备宽度)
-const canvasWidth = ref(700) // 初始值,会在 mounted 时覆盖
-const canvasHeight = ref(280)
-
-// 获取Canvas实际尺寸的函数 - 参考微信小程序示例使用固定尺寸
-function getCanvasSize(): Promise<{ width: number; height: number }> {
-  return new Promise((resolve) => {
-    // 使用固定尺寸,参考微信小程序示例
-    const windowWidth = uni.getSystemInfoSync().windowWidth;
-    const width = windowWidth; // 占满屏幕宽度
-    const height = 280 / 750 * windowWidth; // 280rpx转换为px,与CSS高度匹配
-    resolve({ width, height });
-  });
-}
-
-// 使用 formatPickerDate 从 src/utils/date.ts
-
-const displayYear = computed(() => current.value.getFullYear())
-const displayMonth = computed(() => current.value.getMonth() + 1)
-
-// 显示周期的计算属性
-const displayPeriod = computed(() => {
-  if (viewMode.value === 'month') {
-    return `${displayYear.value}年 ${displayMonth.value}月`
-  } else {
-    // 周视图:显示该周的日期范围
-    const weekStart = getWeekStart(current.value)
-    const weekEnd = getWeekEnd(current.value)
-    return `${formatDisplayDate(weekStart)} - ${formatDisplayDate(weekEnd)}`
-  }
-})
-
-const records = ref<RecordItem[]>(generateMockRecords(current.value))
-
-function generateMockRecords(d: Date): RecordItem[] {
-  const arr: RecordItem[] = []
-  let daysCount: number
-  
-  if (viewMode.value === 'month') {
-    // 月视图:生成该月的记录
-    const y = d.getFullYear()
-    const m = d.getMonth()
-    daysCount = daysInMonth(y, m)
-  } else {
-    // 周视图:生成该周的记录(7天)
-    daysCount = 7
-  }
-  
-  const n = Math.floor(Math.random() * Math.min(daysCount, 7)) // 最多7条记录
-  for (let i = 0; i < n; i++) {
-    let recordDate: Date
-    
-    if (viewMode.value === 'month') {
-      const y = d.getFullYear()
-      const m = d.getMonth()
-      const day = Math.max(1, Math.floor(Math.random() * daysCount) + 1)
-      recordDate = new Date(y, m, day)
-    } else {
-      // 周视图:从周一开始
-      const weekStart = getWeekStart(d)
-      const dayOffset = Math.floor(Math.random() * 7)
-      recordDate = new Date(weekStart)
-      recordDate.setDate(weekStart.getDate() + dayOffset)
-    }
-    
-    arr.push({ 
-      id: `${recordDate.getTime()}${i}${Date.now()}`, 
-      date: formatDisplayDate(recordDate), 
-      height: 150 + Math.floor(Math.random() * 50)
-    })
-  }
-  return arr.sort((a, b) => (a.date < b.date ? 1 : -1))
-}
-
-// 将 records 聚合为每天一个点(取最新记录)
-function aggregateDaily(recordsArr: RecordItem[], year: number, month: number) {
-  const map = new Map<number, RecordItem>()
-  for (const r of recordsArr) {
-    const parts = r.date.split('-')
-    if (parts.length >= 3) {
-      const y = parseInt(parts[0], 10)
-      const m = parseInt(parts[1], 10) - 1
-      const d = parseInt(parts[2], 10)
-      if (y === year && m === month) {
-        // 覆盖同一天,保留最新的(数组头部为最新)
-        map.set(d, r)
-      }
-    }
-  }
-  // 返回按日索引的数组
-  return map
-}
-
-const averageHeight = computed(() => {
-  if (records.value.length === 0) return '--'
-  const sum = records.value.reduce((s, r) => s + r.height, 0)
-  return (sum / records.value.length).toFixed(1)
-})
-
-// 使用共享日期工具函数 (src/utils/date.ts)
-
-// 使用 createUChart composable
-const vm = getCurrentInstance()
-const heightChart = createUChart({
-  canvasId: 'heightChart',
-  vm,
-  getCanvasSize,
-  seriesNames: '身高',
-  valueAccessors: r => r.height,
-  colors: '#ff6a00'
-})
-
-
-
-// 生命周期钩子
-onMounted(() => {
-  // 延迟确保DOM渲染完成
-  setTimeout(async () => {
-    await nextTick()
-    // 由 getCanvasSize 计算并覆盖 canvasWidth/canvasHeight(确保模板样式和绘图一致)
-    try {
-      const size = await getCanvasSize()
-      canvasWidth.value = size.width
-      canvasHeight.value = size.height
-    } catch (e) {
-      console.warn('getCanvasSize failed on mounted', e)
-    }
-    await heightChart.draw(records, current, viewMode)
-  }, 500)
-})
-
-// 简化监听,避免频繁重绘
-watch([() => current.value], async () => {
-  setTimeout(async () => {
-    await heightChart.update(records, current, viewMode)
-  }, 100)
-})
-
-watch([() => records.value], async () => {
-  setTimeout(async () => {
-    await heightChart.update(records, current, viewMode)
-  }, 100)
-}, { deep: true })
-
-onBeforeUnmount(() => {
-  heightChart.destroy()
-})
-
-// 其他函数保持不变
-async function prevPeriod() {
-  const d = new Date(current.value)
-  if (viewMode.value === 'month') {
-    d.setMonth(d.getMonth() - 1)
-  } else {
-    d.setDate(d.getDate() - 7) // 前一周
-  }
-  current.value = d
-  pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
-  records.value = generateMockRecords(d)
-  await heightChart.rebuild(records, current, viewMode)
-}
-
-async function nextPeriod() {
-  const d = new Date(current.value)
-  if (viewMode.value === 'month') {
-    d.setMonth(d.getMonth() + 1)
-  } else {
-    d.setDate(d.getDate() + 7) // 后一周
-  }
-  current.value = d
-  pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
-  records.value = generateMockRecords(d)
-  await heightChart.rebuild(records, current, viewMode)
-}
-
-async function setViewMode(mode: 'month' | 'week') {
-  if (viewMode.value !== mode) {
-    viewMode.value = mode
-    // 重新生成数据和图表
-    records.value = generateMockRecords(current.value)
-    await heightChart.rebuild(records, current, viewMode)
-  }
-}
-
-async function onPickerChange(e: any) {
-  const val = e?.detail?.value || e
-  if (Array.isArray(val) && val.length >= 2) {
-    const y = 2000 + val[0]
-    const m = val[1]
-    const d = new Date(y, m, 1)
-    current.value = d
-    pickerValue.value = [val[0], val[1]]
-    records.value = generateMockRecords(d)
-    await heightChart.rebuild(records, current, viewMode)
-  }
-}
-
-// 添加逻辑保持不变
-const showAdd = ref(false)
-const addDate = ref(formatPickerDate(new Date()))
-const addDateLabel = ref(formatDisplayDate(new Date()))
-const addHeight = ref<number | null>(null)
-
-function onAddRulerUpdate(val: number) {
-  addHeight.value = Number(val.toFixed(1))
-}
-
-function onAddRulerChange(val: number) {
-  addHeight.value = Number(val.toFixed(1))
-}
-
-function openAdd() {
-  showAdd.value = true
-  if (!addHeight.value) addHeight.value = 170
-}
-
-function closeAdd() {
-  showAdd.value = false
-  addHeight.value = null
-}
-
-function onAddDateChange(e: any) {
-  const val = e?.detail?.value || e
-  addDate.value = val
-  addDateLabel.value = val.replace(/^(.{10}).*$/, '$1')
-}
-
-async function confirmAdd() {
-  if (!addHeight.value) {
-    uni.showToast && uni.showToast({ title: '请输入身高', icon: 'none' })
-    return
-  }
-  const id = `user-${Date.now()}`
-  const item: RecordItem = { id, date: addDateLabel.value, height: Number(Number(addHeight.value).toFixed(1)) }
-  const parts = addDate.value.split('-')
-  const addY = parseInt(parts[0], 10)
-  const addM = parseInt(parts[1], 10) - 1
-  const addD = parseInt(parts[2], 10)
-  const addDateObj = new Date(addY, addM, addD)
-  
-  // 检查是否在当前周期内
-  let isInCurrentPeriod = false
-  if (viewMode.value === 'month') {
-    isInCurrentPeriod = addY === current.value.getFullYear() && addM === current.value.getMonth()
-  } else {
-    const weekStart = getWeekStart(current.value)
-    const recordWeekStart = getWeekStart(addDateObj)
-    isInCurrentPeriod = weekStart.getTime() === recordWeekStart.getTime()
-  }
-  
-  if (isInCurrentPeriod) {
-    records.value = [item, ...records.value]
-  }
-  uni.showToast && uni.showToast({ title: '已添加', icon: 'success' })
-  closeAdd()
-  // 新增记录后彻底重建图表,确保像退出再进入一样刷新
-  try {
-    await heightChart.rebuild(records, current, viewMode)
-  } catch (e) {
-    console.warn('rebuildChart after add failed', e)
-  }
-}
-
-async function confirmDeleteRecord(id: string) {
-  if (typeof uni !== 'undefined' && uni.showModal) {
-    uni.showModal({ 
-      title: '删除', 
-      content: '确认删除该条记录吗?', 
-      success: async (res: any) => { 
-        if (res.confirm) {
-          records.value = records.value.filter(r => r.id !== id)
-          try { await heightChart.rebuild(records, current, viewMode) } catch (e) { console.warn('rebuildChart after delete failed', e) }
-        }
-      }
-    })
-  } else {
-    records.value = records.value.filter(r => r.id !== id)
-    try { await heightChart.rebuild(records, current, viewMode) } catch (e) { console.warn('rebuildChart after delete failed', e) }
-  }
-}
-</script>
-
-<style scoped>
-.page { 
-  min-height: calc(100vh); 
-  padding-top: calc(var(--status-bar-height) + 44px); 
-  background: #f5f6f8; 
-  box-sizing: border-box 
-}
-
-.header { 
-  padding: 20rpx 40rpx 
-}
-
-.month-selector { 
-  display: flex; 
-  align-items: center; 
-  justify-content: center; 
-  gap: 12rpx 
-}
-
-.period-controls {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 8rpx;
-}
-
-.view-toggle {
-  display: flex;
-  gap: 4rpx;
-}
-
-.toggle-btn {
-  padding: 4rpx 12rpx;
-  border: 1rpx solid #ddd;
-  background: #f5f5f5;
-  color: #666;
-  border-radius: 6rpx;
-  font-size: 24rpx;
-  min-width: 60rpx;
-  text-align: center;
-}
-
-.toggle-btn.active {
-  background: #ff6a00;
-  color: #fff;
-  border-color: #ff6a00;
-}
-
-.month-label { 
-  font-size: 34rpx; 
-  color: #333 
-}
-
-.btn { 
-  background: transparent; 
-  border: none; 
-  font-size: 36rpx; 
-  color: #666 
-}
-
-.content { 
-  padding: 20rpx 24rpx 100rpx 24rpx 
-}
-
-.chart-wrap { 
-  height: 340rpx;
-  overflow: hidden; /* 隐藏溢出内容 */
-  background: #fff; 
-  border-radius: 12rpx; 
-  padding: 24rpx; 
-  margin: 0 24rpx 20rpx 24rpx; 
-  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03) 
-}
-
-.chart-header { 
-  font-size: 32rpx; 
-  color: #333; 
-  margin-bottom: 20rpx; 
-  font-weight: 600 
-}
-
-/* 关键修复:确保canvas样式正确,参考微信小程序示例 */
-.chart-canvas {
-  height: 280rpx;
-  background-color: #FFFFFF;
-  display: block;
-}
-
-.summary { 
-  padding: 20rpx; 
-  color: #666; 
-  font-size: 28rpx 
-}
-
-.list { 
-  background: #fff; 
-  border-radius: 12rpx; 
-  padding: 10rpx; 
-  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03) 
-}
-
-.empty { 
-  padding: 40rpx; 
-  text-align: center; 
-  color: #999 
-}
-
-.list-item { 
-  display: flex; 
-  align-items: center; 
-  padding: 20rpx; 
-  border-bottom: 1rpx solid #f0f0f0 
-}
-
-.list-item .date { 
-  color: #666 
-}
-
-.list-item .value { 
-  color: #333; 
-  font-weight: 600; 
-  flex: 1; 
-  text-align: right 
-}
-
-.btn-delete { 
-  width: 80rpx; 
-  height: 60rpx; 
-  min-width: 60rpx; 
-  min-height: 60rpx; 
-  display: inline-flex; 
-  align-items: center; 
-  justify-content: center; 
-  background: #fff0f0; 
-  color: #d9534f; 
-  border: 1rpx solid rgba(217,83,79,0.15); 
-  border-radius: 8rpx; 
-  margin-left: 30rpx 
-}
-
-.fab { 
-  position: fixed; 
-  right: 28rpx; 
-  bottom: 160rpx; 
-  width: 110rpx; 
-  height: 110rpx; 
-  border-radius: 999px; 
-  background: linear-gradient(180deg, #ff7a00, #ff4a00); 
-  display: flex; 
-  align-items: center; 
-  justify-content: center; 
-  box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.2); 
-  z-index: 1200 
-}
-
-.fab-inner { 
-  color: #fff; 
-  font-size: 56rpx; 
-  line-height: 56rpx 
-}
-
-.modal { 
-  position: fixed; 
-  left: 0; 
-  right: 0; 
-  top: 0; 
-  bottom: 0; 
-  display: flex; 
-  align-items: flex-end; 
-  justify-content: center; 
-  z-index: 1300 
-}
-
-.modal-backdrop { 
-  position: absolute; 
-  left: 0; 
-  right: 0; 
-  top: 0; 
-  bottom: 0; 
-  background: rgba(0, 0, 0, 0.4) 
-}
-
-.modal-panel { 
-  position: relative; 
-  width: 100%; 
-  background: #fff; 
-  border-top-left-radius: 18rpx; 
-  border-top-right-radius: 18rpx; 
-  padding: 28rpx 24rpx 140rpx 24rpx; 
-  box-shadow: 0 -8rpx 30rpx rgba(0,0,0,0.12) 
-}
-
-.modal-title { 
-  font-size: 56rpx; 
-  margin-block: 60rpx; 
-  color: #222; 
-  font-weight: 700; 
-  letter-spacing: 1rpx 
-}
-
-.modal-inner { 
-  max-width: 70%; 
-  margin: 0 auto 
-}
-
-.form-row { 
-  display: flex; 
-  align-items: center; 
-  justify-content: space-between; 
-  margin-bottom: 34rpx; 
-  padding: 14rpx 0; 
-  font-size: 32rpx 
-}
-
-.input { 
-  width: 150rpx; 
-  text-align: right; 
-  padding: 16rpx; 
-  border-radius: 14rpx; 
-  border: 1rpx solid #eee; 
-  background: #fff7f0 
-}
-
-.picker-display { 
-  color: #333 
-}
-
-.btn-primary { 
-  background: #ff6a00; 
-  color: #fff; 
-  padding: 18rpx 22rpx; 
-  border-radius: 16rpx; 
-  text-align: center; 
-  width: 50%; 
-  box-shadow: 0 10rpx 28rpx rgba(255,106,0,0.18) 
-}
-
-.drag-handle { 
-  width: 64rpx; 
-  height: 6rpx; 
-  background: rgba(0,0,0,0.08); 
-  border-radius: 999px; 
-  margin: 10rpx auto 14rpx auto 
-}
-
-.modal-header { 
-  display: flex; 
-  align-items: center; 
-  justify-content: center; 
-  gap: 12rpx; 
-  margin-bottom: 6rpx 
-}
-
-.label { 
-  color: #666 
-}
-
-.ruler-wrap { 
-  margin: 12rpx 0 
-}
-
-.fixed-footer { 
-  position: absolute; 
-  left: 0; 
-  right: 0; 
-  bottom: 40rpx; 
-  padding: 0 24rpx 
-}
-
-.btn-full { 
-  width: 100%; 
-  padding: 18rpx; 
-  border-radius: 12rpx; 
-}
-</style>

+ 236 - 102
src/pages/patient/health/details/weight.vue → src/pages/patient/health/details/physical.vue

@@ -1,5 +1,5 @@
 <template>
-  <CustomNav title="体" leftType="back" />
+  <CustomNav title="体格数据" leftType="back" />
   <view class="page">
 
     <view class="header">
@@ -13,6 +13,12 @@
             <button :class="['toggle-btn', { active: viewMode === 'month' }]" @click="setViewMode('month')">月</button>
             <button :class="['toggle-btn', { active: viewMode === 'week' }]" @click="setViewMode('week')">周</button>
           </view>
+                  <view class="metric-toggle" style="margin-top:8rpx; display:flex; gap:8rpx;">
+                    <button :class="['toggle-btn', { active: selectedMetric === 'all' }]" @click.prevent="selectedMetric = 'all'">全部</button>
+                    <button :class="['toggle-btn', { active: selectedMetric === 'height' }]" @click.prevent="selectedMetric = 'height'">身高</button>
+                    <button :class="['toggle-btn', { active: selectedMetric === 'weight' }]" @click.prevent="selectedMetric = 'weight'">体重</button>
+                    <button :class="['toggle-btn', { active: selectedMetric === 'bmi' }]" @click.prevent="selectedMetric = 'bmi'">BMI</button>
+                  </view>
         </view>
         <button class="btn" @click="nextPeriod">›</button>
       </view>
@@ -20,23 +26,23 @@
 
     <!-- 趋势图 - 简化canvas设置 -->
     <view class="chart-wrap">
-      <view class="chart-header">本月趋势</view>
+  <view class="chart-header">本月趋势</view>
       <canvas 
-        canvas-id="weightChart" 
-        id="weightChart" 
+        canvas-id="bpChart" 
+        id="bpChart" 
         class="chart-canvas"
         :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
       ></canvas>
     </view>
 
-    <!-- 其他内容保持不变 -->
     <view class="content">
-      <view class="summary">共 {{ records.length }} 条记录,本月平均:{{ averageWeight }} kg</view>
+  <view class="summary">共 {{ records.length }} 条记录,本月平均:{{ averageHeight }} cm / {{ averageWeight }} kg;平均 BMI:{{ averageBMI }}</view>
+
       <view class="list">
         <view v-if="records.length === 0" class="empty">暂无记录,点击右下角 + 添加</view>
-        <view v-for="item in records" :key="item.id" class="list-item">
+        <view v-for="item in records" :key="item.id" class="list-item" :style="{ backgroundColor: getItemColor(item.h, item.w) }">
           <view class="date">{{ item.date }}</view>
-          <view class="value">{{ item.weight }} kg</view>
+          <view class="value">{{ item.h }} cm / {{ item.w }} kg · BMI {{ item.bmi }}</view>
           <button class="btn-delete" @click="confirmDeleteRecord(item.id)">✕</button>
         </view>
       </view>
@@ -48,9 +54,9 @@
 
     <view class="modal" v-if="showAdd">
       <view class="modal-backdrop" @click="closeAdd"></view>
-      <view class="modal-panel">
+  <view class="modal-panel">
         <view class="drag-handle"></view>
-        <view class="modal-header"><text class="modal-title">添加体</text></view>
+  <view class="modal-header"><text class="modal-title">添加体格数据</text></view>
 
         <view class="modal-inner">
           <view class="form-row">
@@ -60,14 +66,24 @@
             </picker>
           </view>
 
+          <view class="form-row">
+            <text class="label">身高 (cm)</text>
+            <input type="number" v-model.number="addHeight" class="input" placeholder="身高 (cm)" />
+          </view>
           <view class="form-row">
             <text class="label">体重 (kg)</text>
-            <input type="number" v-model.number="addWeight" class="input" placeholder="请输入体重" />
+            <input type="number" v-model.number="addWeight" class="input" placeholder="体重 (kg)" />
           </view>
         </view>
 
+        <!--提供滑动条(收缩压70-200,舒张压40-120)-->
         <view class="ruler-wrap">
-          <ScaleRuler v-if="showAdd" :min="20" :max="200" :step="0.1" :gutter="20" :initialValue="addWeight ?? 65" @update="onAddRulerUpdate" @change="onAddRulerChange" />
+          <view class="ruler-row">
+            <ScaleRuler v-if="showAdd" :min="100" :max="220" :step="1" :gutter="16" :initialValue="addHeight ?? 170" @update="onHUpdate" @change="onHChange" />
+          </view>
+          <view class="ruler-row">
+            <ScaleRuler v-if="showAdd" :min="30" :max="200" :step="1" :gutter="16" :initialValue="addWeight ?? 65" @update="onWUpdate" @change="onWChange" />
+          </view>
         </view>
 
         <view class="fixed-footer">
@@ -75,6 +91,7 @@
         </view>
       </view>
     </view>
+
   </view>
 
 </template>
@@ -85,26 +102,27 @@ import { createUChart } from '@/composables/useUChart'
 import CustomNav from '@/components/custom-nav.vue'
 
 import ScaleRuler from '@/components/scale-ruler.vue'
-import { getWeekStart, getWeekEnd,formatDisplayDate, formatPickerDate, daysInMonth } from '@/utils/date'
+import { getWeekStart, getWeekEnd, formatDisplayDate, formatPickerDate, daysInMonth } from '@/utils/date'
 
-type RecordItem = { id: string; date: string; weight: number }
+type RecordItem = { id: string; date: string; h: number; w: number; bmi: number }
 
 // 当前展示年月
 const current = ref(new Date())
-const pickerValue = ref([current.value.getFullYear() - 2000, current.value.getMonth()]) // 年从2000年开始,月0-11
+// 使用 multiSelector 的索引形式: [yearOffset从2000起, month(0-11)]
+const pickerValue = ref([current.value.getFullYear() - 2000, current.value.getMonth()])
 
 // 视图模式:'month' 或 'week'
 const viewMode = ref<'month' | 'week'>('month')
 
-// 年月选择器的选项范围
+// 年月选择器的选项范围(与 height/weight 保持一致)
 const pickerRange = ref([
-  Array.from({ length: 50 }, (_, i) => `${2000 + i}年`), // 2000-2049年
-  Array.from({ length: 12 }, (_, i) => `${i + 1}月`) // 1-12月
+  Array.from({ length: 50 }, (_, i) => `${2000 + i}年`),
+  Array.from({ length: 12 }, (_, i) => `${i + 1}月`)
 ])
 
 // 明确的canvas尺寸(将由 getCanvasSize 初始化以匹配设备宽度)
 const canvasWidth = ref(700) // 初始值,会在 mounted 时覆盖
-const canvasHeight = ref(280)
+const canvasHeight = ref(320)
 
 // 获取Canvas实际尺寸的函数 - 参考微信小程序示例使用固定尺寸
 function getCanvasSize(): Promise<{ width: number; height: number }> {
@@ -112,7 +130,7 @@ function getCanvasSize(): Promise<{ width: number; height: number }> {
     // 使用固定尺寸,参考微信小程序示例
     const windowWidth = uni.getSystemInfoSync().windowWidth;
     const width = windowWidth; // 占满屏幕宽度
-    const height = 280 / 750 * windowWidth; // 280rpx转换为px,与CSS高度匹配
+    const height = 320 / 750 * windowWidth; // 320rpx转换为px,与CSS高度匹配
     resolve({ width, height });
   });
 }
@@ -122,7 +140,7 @@ function getCanvasSize(): Promise<{ width: number; height: number }> {
 const displayYear = computed(() => current.value.getFullYear())
 const displayMonth = computed(() => current.value.getMonth() + 1)
 
-// 显示周期的计算属性
+// 显示周期(支持月/周)
 const displayPeriod = computed(() => {
   if (viewMode.value === 'month') {
     return `${displayYear.value}年 ${displayMonth.value}月`
@@ -137,35 +155,31 @@ const records = ref<RecordItem[]>(generateMockRecords(current.value))
 
 function generateMockRecords(d: Date): RecordItem[] {
   const arr: RecordItem[] = []
-  let daysCount: number
-
   if (viewMode.value === 'month') {
     const y = d.getFullYear()
     const m = d.getMonth()
-    daysCount = daysInMonth(y, m)
+    const n = Math.floor(Math.random() * Math.min(daysInMonth(y, m), 7))
+    for (let i = 0; i < n; i++) {
+      const day = Math.max(1, Math.floor(Math.random() * daysInMonth(y, m)) + 1)
+      const date = new Date(y, m, day)
+      // 随机生成身高(150-190)和体重(45-100)作为示例数据
+  const h = 150 + Math.floor(Math.random() * 40)
+  const w = 45 + Math.floor(Math.random() * 55)
+  const bmi = Math.round((w / ((h / 100) * (h / 100))) * 10) / 10
+  arr.push({ id: `${date.getTime()}-${i}`, date: formatDisplayDate(date), h, w, bmi })
+    }
   } else {
-    daysCount = 7
-  }
-
-  const n = Math.floor(Math.random() * Math.min(daysCount, 7))
-  for (let i = 0; i < n; i++) {
-    let recordDate: Date
-    if (viewMode.value === 'month') {
-      const y = d.getFullYear()
-      const m = d.getMonth()
-      const day = Math.max(1, Math.floor(Math.random() * daysCount) + 1)
-      recordDate = new Date(y, m, day)
-    } else {
-      const weekStart = getWeekStart(d)
+    const weekStart = getWeekStart(d)
+    const n = Math.floor(Math.random() * 7)
+    for (let i = 0; i < n; i++) {
       const dayOffset = Math.floor(Math.random() * 7)
-      recordDate = new Date(weekStart)
-      recordDate.setDate(weekStart.getDate() + dayOffset)
+      const date = new Date(weekStart)
+      date.setDate(weekStart.getDate() + dayOffset)
+  const h = 150 + Math.floor(Math.random() * 40)
+  const w = 45 + Math.floor(Math.random() * 55)
+  const bmi = Math.round((w / ((h / 100) * (h / 100))) * 10) / 10
+  arr.push({ id: `${date.getTime()}-${i}`, date: formatDisplayDate(date), h, w, bmi })
     }
-    arr.push({
-      id: `${recordDate.getTime()}${i}${Date.now()}`,
-      date: formatDisplayDate(recordDate),
-      weight: Number((50 + Math.random() * 50).toFixed(1))
-    })
   }
   return arr.sort((a, b) => (a.date < b.date ? 1 : -1))
 }
@@ -189,26 +203,79 @@ function aggregateDaily(recordsArr: RecordItem[], year: number, month: number) {
   return map
 }
 
+const averageHeight = computed(() => {
+  if (records.value.length === 0) return '--'
+  const sum = records.value.reduce((s, r) => s + r.h, 0)
+  return Math.round(sum / records.value.length)
+})
 const averageWeight = computed(() => {
   if (records.value.length === 0) return '--'
-  const sum = records.value.reduce((s, r) => s + r.weight, 0)
-  return (sum / records.value.length).toFixed(1)
+  const sum = records.value.reduce((s, r) => s + r.w, 0)
+  return Math.round(sum / records.value.length)
 })
+const averageBMI = computed(() => {
+  if (records.value.length === 0) return '--'
+  const sum = records.value.reduce((s, r) => s + (r.bmi || 0), 0)
+  return Math.round((sum / records.value.length) * 10) / 10
+})
+
+// 根据血压值获取颜色
+function getItemColor(h: number, w: number): string {
+  // 根据 BMI 简单分类
+  if (!h || !w) return '#ffffff'
+  const bmi = w / ((h / 100) * (h / 100))
+  if (bmi < 18.5) return '#fff3cd' // 偏瘦 - 黄
+  if (bmi < 25) return '#e8f5e8' // 正常 - 绿
+  if (bmi < 30) return '#fff3cd' // 超重 - 黄
+  return '#f8d7da' // 肥胖 - 红
+}
+
 // 使用共享日期工具 (src/utils/date.ts)
 
-// 使用 createUChart composable (替换本地 uCharts 管理)
+// 使用可复用的 chart composable,支持多序列
 const vm = getCurrentInstance()
-const weightChart = createUChart({
-  canvasId: 'weightChart',
-  vm,
-  getCanvasSize,
-  seriesNames: '体重',
-  valueAccessors: r => r.weight,
-  colors: '#ff6a00'
-})
 
-// 生命周期:初始化 / 更新 / 销毁 通过 composable 调用
+// 选择显示的指标:'all' | 'height' | 'weight' | 'bmi'
+const selectedMetric = ref<'all'|'height'|'weight'|'bmi'>('all')
+
+let bpChart: any = null
+
+function createChartForMetric(metric: 'all'|'height'|'weight'|'bmi') {
+  const seriesMap: Record<string, { names: string[]; accessors: Array<(r:any)=>number>; colors: string[] }> = {
+    all: { names: ['身高','体重','BMI'], accessors: [ (r: RecordItem) => r.h, (r: RecordItem) => r.w, (r: RecordItem) => r.bmi ], colors: ['#ff6a00', '#007aff', '#28c76f'] },
+    height: { names: ['身高'], accessors: [ (r: RecordItem) => r.h ], colors: ['#ff6a00'] },
+    weight: { names: ['体重'], accessors: [ (r: RecordItem) => r.w ], colors: ['#007aff'] },
+    bmi: { names: ['BMI'], accessors: [ (r: RecordItem) => r.bmi ], colors: ['#28c76f'] }
+  }
+  const cfg = seriesMap[metric]
+  return createUChart({
+    canvasId: 'bpChart',
+    vm,
+    getCanvasSize,
+    seriesNames: cfg.names,
+    valueAccessors: cfg.accessors,
+    colors: cfg.colors
+  })
+}
+
+// 延迟创建 chart:在 mounted 时先读取路由参数(metric),然后创建 chart 并绘制
+function initMetricFromRoute() {
+  try {
+    // 在 uni-app 中,可通过 getCurrentPages 获取当前页面的 options(包含 query)
+    const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []
+    const currentPage = pages[pages.length - 1] || {}
+  const opts = (currentPage && (currentPage as any).options) ? (currentPage as any).options : {}
+  const metricParam = opts.metric || opts?.metric
+    if (metricParam && ['all', 'height', 'weight', 'bmi'].includes(metricParam)) {
+      ;(selectedMetric as any).value = metricParam
+    }
+  } catch (e) {
+    console.warn('initMetricFromRoute error', e)
+  }
+}
+
 onMounted(() => {
+  // 延迟确保DOM渲染完成并设置canvas尺寸
   setTimeout(async () => {
     await nextTick()
     try {
@@ -218,27 +285,66 @@ onMounted(() => {
     } catch (e) {
       console.warn('getCanvasSize failed on mounted', e)
     }
-    await weightChart.draw(records, current, viewMode)
+
+    // 先从路由读取 metric(如果有),然后创建 chart
+    initMetricFromRoute()
+    try {
+      if (!bpChart) bpChart = createChartForMetric(selectedMetric.value)
+    } catch (e) {
+      console.warn('createChartForMetric failed', e)
+    }
+
+    try {
+      if (bpChart && bpChart.draw) await bpChart.draw(records, current, viewMode)
+    } catch (e) {
+      console.warn('bpChart draw failed', e)
+    }
   }, 500)
 })
 
+// 监听并更新图表(轻微去抖)
 watch([() => current.value], async () => {
   setTimeout(async () => {
-    await weightChart.update(records, current, viewMode)
+    await bpChart.update(records, current, viewMode)
   }, 100)
 })
 
 watch([() => records.value], async () => {
   setTimeout(async () => {
-    await weightChart.update(records, current, viewMode)
+    // 如果图表实例已被替换,根据 current selectedMetric 的 chart 实例更新
+    try {
+      if (bpChart && bpChart.update) await bpChart.update(records, current, viewMode)
+    } catch (e) {
+      console.warn('bpChart update failed', e)
+    }
   }, 100)
 }, { deep: true })
 
+// 监听 selectedMetric 变化,重建图表以使用不同的 series 配置
+watch(() => selectedMetric.value, async (val) => {
+  try {
+    if (bpChart && bpChart.destroy) {
+      try { bpChart.destroy() } catch (e) { /* ignore */ }
+    }
+    bpChart = createChartForMetric(val)
+    // small delay to ensure DOM/canvas availability
+    await nextTick()
+    try { await bpChart.draw(records, current, viewMode) } catch (e) { console.warn('draw after metric change failed', e) }
+  } catch (e) {
+    console.warn('selectedMetric watch error', e)
+  }
+})
+
 onBeforeUnmount(() => {
-  weightChart.destroy()
+  try { bpChart.destroy() } catch (e) { console.warn('bpChart destroy error', e) }
 })
 
-// 周/月切换和周期导航
+// 强制重建图表(用于切换月份时彻底刷新)
+async function rebuildChart() {
+  try { if (bpChart && bpChart.rebuild) await bpChart.rebuild(records, current, viewMode) } catch (e) { console.warn('rebuildChart failed', e) }
+}
+
+// 周/月周期导航与 Picker 处理
 async function prevPeriod() {
   const d = new Date(current.value)
   if (viewMode.value === 'month') {
@@ -249,7 +355,7 @@ async function prevPeriod() {
   current.value = d
   pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
   records.value = generateMockRecords(d)
-  await weightChart.rebuild(records, current, viewMode)
+  await rebuildChart()
 }
 
 async function nextPeriod() {
@@ -262,14 +368,14 @@ async function nextPeriod() {
   current.value = d
   pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
   records.value = generateMockRecords(d)
-  await weightChart.rebuild(records, current, viewMode)
+  await rebuildChart()
 }
 
 async function setViewMode(mode: 'month' | 'week') {
   if (viewMode.value !== mode) {
     viewMode.value = mode
     records.value = generateMockRecords(current.value)
-    await weightChart.rebuild(records, current, viewMode)
+    await rebuildChart()
   }
 }
 
@@ -282,79 +388,104 @@ async function onPickerChange(e: any) {
     current.value = d
     pickerValue.value = [val[0], val[1]]
     records.value = generateMockRecords(d)
-    await weightChart.rebuild(records, current, viewMode)
+    await rebuildChart()
   }
 }
 
-// 添加逻辑保持不变
+// 添加逻辑(体格数据)
 const showAdd = ref(false)
 const addDate = ref(formatPickerDate(new Date()))
 const addDateLabel = ref(formatDisplayDate(new Date()))
+const addHeight = ref<number | null>(null)
 const addWeight = ref<number | null>(null)
 
-function onAddRulerUpdate(val: number) {
-  addWeight.value = Number(val.toFixed(1))
-}
+function onHUpdate(v: number) { addHeight.value = Math.round(v) }
+function onHChange(v: number) { addHeight.value = Math.round(v) }
+function onWUpdate(v: number) { addWeight.value = Math.round(v) }
+function onWChange(v: number) { addWeight.value = Math.round(v) }
 
-function onAddRulerChange(val: number) {
-  addWeight.value = Number(val.toFixed(1))
+function openAdd() { 
+  showAdd.value = true; 
+  if (!addHeight.value) addHeight.value = 170; 
+  if (!addWeight.value) addWeight.value = 65 
 }
-
-function openAdd() {
-  showAdd.value = true
-  if (!addWeight.value) addWeight.value = 65
+function closeAdd() { 
+  showAdd.value = false; 
+  addHeight.value = null; 
+  addWeight.value = null 
 }
 
-function closeAdd() {
-  showAdd.value = false
-  addWeight.value = null
-}
-
-function onAddDateChange(e: any) {
-  const val = e?.detail?.value || e
-  addDate.value = val
-  addDateLabel.value = val.replace(/^(.{10}).*$/, '$1')
+function onAddDateChange(e: any) { 
+  const val = e?.detail?.value || e; 
+  addDate.value = val; 
+  addDateLabel.value = val.replace(/^(.{10}).*$/, '$1') 
 }
 
 async function confirmAdd() {
-  if (!addWeight.value) {
-    uni.showToast && uni.showToast({ title: '请输入体重', icon: 'none' })
-    return
+  if (!addHeight.value || !addWeight.value) { 
+    uni.showToast && uni.showToast({ title: '请输入身高和体重', icon: 'none' }); 
+    return 
+  }
+  // 检查 BMI 预警(简单阈值)
+  const bmi = addWeight.value / ((addHeight.value / 100) * (addHeight.value / 100))
+  if (bmi >= 25) {
+    uni.showModal({
+      title: '体重超标',
+      content: '当前体重指数偏高,建议注意饮食与运动,必要时就医。',
+      showCancel: false,
+      confirmText: '知道了'
+    })
   }
   const id = `user-${Date.now()}`
-  const item: RecordItem = { id, date: addDateLabel.value, weight: Number(Number(addWeight.value).toFixed(1)) }
+  const bmiVal = Math.round((Math.round(addWeight.value) / ((Math.round(addHeight.value) / 100) * (Math.round(addHeight.value) / 100))) * 10) / 10
+  const item: RecordItem = { 
+    id, 
+    date: addDateLabel.value, 
+    h: Math.round(addHeight.value), 
+    w: Math.round(addWeight.value),
+    bmi: bmiVal
+  }
   const parts = addDate.value.split('-')
   const addY = parseInt(parts[0], 10)
   const addM = parseInt(parts[1], 10) - 1
-  if (addY === current.value.getFullYear() && addM === current.value.getMonth()) {
-    records.value = [item, ...records.value]
+  if (viewMode.value === 'month') {
+    if (addY === current.value.getFullYear() && addM === current.value.getMonth()) {
+      records.value = [item, ...records.value]
+    }
+  } else {
+    const addDateObj = new Date(addY, addM, parseInt(parts[2] || '1', 10))
+    const addWeekStart = getWeekStart(addDateObj)
+    const curWeekStart = getWeekStart(current.value)
+    if (addWeekStart.getTime() === curWeekStart.getTime()) {
+      records.value = [item, ...records.value]
+    }
   }
   uni.showToast && uni.showToast({ title: '已添加', icon: 'success' })
   closeAdd()
   // 新增记录后彻底重建图表,确保像退出再进入一样刷新
   try {
-    await weightChart.rebuild(records, current, viewMode)
+    await rebuildChart()
   } catch (e) {
     console.warn('rebuildChart after add failed', e)
   }
 }
 
-async function confirmDeleteRecord(id: string) {
-  if (typeof uni !== 'undefined' && uni.showModal) {
+async function confirmDeleteRecord(id: string) { 
+  if (typeof uni !== 'undefined' && uni.showModal) { 
     uni.showModal({ 
       title: '删除', 
       content: '确认删除该条记录吗?', 
       success: async (res: any) => { 
         if (res.confirm) {
           records.value = records.value.filter(r => r.id !== id)
-          try { await weightChart.rebuild(records, current, viewMode) } catch (e) { console.warn('rebuildChart after delete failed', e) }
+          try { await rebuildChart() } catch (e) { console.warn('rebuildChart after delete failed', e) }
         }
-      }
-    })
-  } else {
+      } 
+    }) 
+  } else { 
     records.value = records.value.filter(r => r.id !== id)
-    try { await weightChart.rebuild(records, current, viewMode) } catch (e) { console.warn('rebuildChart after delete failed', e) }
-  }
+    try { await rebuildChart() } catch (e) { console.warn('rebuildChart after delete failed', e) }
+  } 
 }
 </script>
 
@@ -423,7 +554,7 @@ async function confirmDeleteRecord(id: string) {
 }
 
 .chart-wrap { 
-  height: 340rpx;
+  height: 380rpx;
   overflow: hidden; /* 隐藏溢出内容 */
   background: #fff; 
   border-radius: 12rpx; 
@@ -441,7 +572,7 @@ async function confirmDeleteRecord(id: string) {
 
 /* 关键修复:确保canvas样式正确,参考微信小程序示例 */
 .chart-canvas {
-  height: 280rpx;
+  height: 320rpx;
   background-color: #FFFFFF;
   display: block;
 }
@@ -519,7 +650,6 @@ async function confirmDeleteRecord(id: string) {
   line-height: 56rpx 
 }
 
-/* 模态框样式保持不变 */
 .modal { 
   position: fixed; 
   left: 0; 
@@ -620,6 +750,10 @@ async function confirmDeleteRecord(id: string) {
   margin: 12rpx 0 
 }
 
+.ruler-row { 
+  margin-bottom: 8rpx 
+}
+
 .fixed-footer { 
   position: absolute; 
   left: 0; 

+ 6 - 5
src/pages/patient/health/index.vue

@@ -3,19 +3,19 @@
   <view class="content">
     <view class="menu-card">
       <view class="menu-list">
-        <view class="menu-item" @click="openDetail('height')">
+        <view class="menu-item" @click="openDetail('physical','height')">
           <image src="/static/icons/remixicon/height.svg" class="menu-icon" mode="widthFix" />
           <text class="menu-text">身高</text>
           <text class="menu-value">--</text>
           <uni-icons class="menu-arrow" type="arrowright" size="20" color="#c0c0c0" />
         </view>
-        <view class="menu-item" @click="openDetail('weight')">
+        <view class="menu-item" @click="openDetail('physical','weight')">
           <image src="/static/icons/remixicon/weight.svg" class="menu-icon" mode="widthFix" />
           <text class="menu-text">体重</text>
           <text class="menu-value">80.0 公斤</text>
           <uni-icons class="menu-arrow" type="arrowright" size="20" color="#c0c0c0" />
         </view>
-        <view class="menu-item" @click="openDetail('bmi')">
+        <view class="menu-item" @click="openDetail('physical','bmi')">
           <image src="/static/icons/remixicon/bmi.svg" class="menu-icon" mode="widthFix" />
           <text class="menu-text">BMI</text>
           <text class="menu-value">--</text>
@@ -59,8 +59,9 @@ import TabBar from '@/components/tab-bar.vue'
 
 const title = ref('健康数据')
 
-const openDetail = (type: string) => {
-  uni.navigateTo({ url: `details/${type}` })
+const openDetail = (type: string, metric?: string) => {
+  const url = metric ? `details/${type}?metric=${metric}` : `details/${type}`
+  uni.navigateTo({ url })
 }
 
 const openReminder = () => {