Просмотр исходного кода

feat(health): 优化身高趋势图绘制与交互体验

- 重构图表绘制逻辑,使用 uCharts 替代原生 canvas 实现
- 动态计算 canvas 尺寸以适配不同设备屏幕宽度
- 固定 pixelRatio 为 1 避免高分辨率下元素过大
- 简化 X 轴标签显示策略,只展示关键日期避免重叠
- 按日聚合数据确保每天仅显示最新记录
- 优化 Y 轴范围自动调整算法提升可视化效果
- 增加重绘锁机制防止并发绘图冲突
- 月份切换时强制重建图表确保数据正确更新
- 调整样式细节改善移动端显示效果
- 修复刻度尺数值精度问题统一保留一位小数
- 完善组件卸载时的资源清理避免内存泄漏
mcbaiyun 2 месяцев назад
Родитель
Сommit
69c7f7f4cb
1 измененных файлов с 672 добавлено и 505 удалено
  1. 672 505
      src/pages/health/details/height.vue

+ 672 - 505
src/pages/health/details/height.vue

@@ -4,21 +4,25 @@
     <view class="header">
     <view class="header">
       <view class="month-selector">
       <view class="month-selector">
         <button class="btn" @click="prevMonth">‹</button>
         <button class="btn" @click="prevMonth">‹</button>
-        <picker mode="date" :value="pickerValue" @change="onPickerChange">
-          <view class="month-label">{{ displayYear }} 年 {{ displayMonth }} 月</view>
-        </picker>
+        <view class="month-label">{{ displayYear }}年 {{ displayMonth }}月</view>
         <button class="btn" @click="nextMonth">›</button>
         <button class="btn" @click="nextMonth">›</button>
       </view>
       </view>
+      <picker mode="date" :value="pickerValue" @change="onPickerChange">
+        <view class="picker-display">切换月份</view>
+      </picker>
     </view>
     </view>
-    <!-- 趋势图:放在月份选择下面 -->
+
+    <!-- 趋势图 - 简化canvas设置 -->
     <view class="chart-wrap">
     <view class="chart-wrap">
       <view class="chart-header">本月趋势</view>
       <view class="chart-header">本月趋势</view>
-      <canvas ref="chartCanvas" id="chartCanvas" canvas-id="heightChart" class="chart-canvas" width="340"
-        height="120"></canvas>
+      <canvas 
+        canvas-id="heightChart" 
+        id="heightChart" 
+        class="chart-canvas"
+        :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
+      ></canvas>
     </view>
     </view>
 
 
-    <!-- 刻度尺控制(已移入添加模态) -->
-
     <view class="content">
     <view class="content">
       <view class="summary">共 {{ records.length }} 条记录,本月平均:{{ averageHeight }} cm</view>
       <view class="summary">共 {{ records.length }} 条记录,本月平均:{{ averageHeight }} cm</view>
 
 
@@ -41,17 +45,13 @@
     <view class="modal" v-if="showAdd">
     <view class="modal" v-if="showAdd">
       <view class="modal-backdrop" @click="closeAdd"></view>
       <view class="modal-backdrop" @click="closeAdd"></view>
       <view class="modal-panel">
       <view class="modal-panel">
-        <!-- Drag handle -->
-        <view class="drag-handle" />
-        <view class="modal-header centered">
-          <view class="modal-title">新增身高记录</view>
-        </view>
+        <view class="drag-handle"></view>
+        <view class="modal-header"><text class="modal-title">添加身高</text></view>
 
 
-        <!-- 将表单与按钮限制在中间的窄容器,便于阅读和操作 -->
-  <view class="modal-inner">
+        <view class="modal-inner">
           <view class="form-row">
           <view class="form-row">
             <text class="label">日期</text>
             <text class="label">日期</text>
-            <picker mode="date" :value="pickerValue" @change="onAddDateChange">
+            <picker mode="date" :value="addDate" @change="onAddDateChange">
               <view class="picker-display">{{ addDateLabel }}</view>
               <view class="picker-display">{{ addDateLabel }}</view>
             </picker>
             </picker>
           </view>
           </view>
@@ -60,15 +60,12 @@
             <text class="label">身高 (cm)</text>
             <text class="label">身高 (cm)</text>
             <input type="number" v-model.number="addHeight" class="input" placeholder="请输入身高" />
             <input type="number" v-model.number="addHeight" class="input" placeholder="请输入身高" />
           </view>
           </view>
-
         </view>
         </view>
 
 
-        <!-- 刻度尺:独立于 .modal-inner,保持屏幕全宽以便手指操作 -->
         <view class="ruler-wrap">
         <view class="ruler-wrap">
-          <ScaleRuler v-if="showAdd" :min="120" :max="220" :step="1" :gutter="16" :initialValue="addHeight ?? 170"
-            @change="onAddRulerChange" @update:value="onAddRulerUpdate" />
+          <ScaleRuler v-if="showAdd" :min="120" :max="220" :step="1" :gutter="16" :initialValue="addHeight ?? 170" @update="onAddRulerUpdate" @change="onAddRulerChange" />
         </view>
         </view>
-        <!-- 固定底部的大保存按钮(距离屏幕底部至少 100rpx) -->
+
         <view class="fixed-footer">
         <view class="fixed-footer">
           <button class="btn-primary btn-full" @click="confirmAdd">保存</button>
           <button class="btn-primary btn-full" @click="confirmAdd">保存</button>
         </view>
         </view>
@@ -80,20 +77,34 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { ref, computed } from 'vue'
+import { ref, computed, onMounted, watch, nextTick, onBeforeUnmount, getCurrentInstance } from 'vue'
+import uCharts from '@qiun/ucharts'
 import CustomNav from '@/components/custom-nav.vue'
 import CustomNav from '@/components/custom-nav.vue'
 import TabBar from '@/components/tab-bar.vue'
 import TabBar from '@/components/tab-bar.vue'
 import ScaleRuler from '@/components/scale-ruler.vue'
 import ScaleRuler from '@/components/scale-ruler.vue'
 
 
 type RecordItem = { id: string; date: string; height: number }
 type RecordItem = { id: string; date: string; height: number }
 
 
-// 当前展示年月(以 JS Date 管理)
+// 当前展示年月
 const current = ref(new Date())
 const current = ref(new Date())
-
 const pickerValue = ref(formatPickerDate(current.value))
 const pickerValue = ref(formatPickerDate(current.value))
 
 
+// 明确的canvas尺寸(将由 getCanvasSize 初始化以匹配设备宽度)
+const canvasWidth = ref(700) // 初始值,会在 mounted 时覆盖
+const canvasHeight = ref(240)
+
+// 获取Canvas实际尺寸的函数 - 参考微信小程序示例使用固定尺寸
+function getCanvasSize(): Promise<{ width: number; height: number }> {
+  return new Promise((resolve) => {
+    // 使用固定尺寸,参考微信小程序示例
+    const windowWidth = uni.getSystemInfoSync().windowWidth;
+    const width = windowWidth; // 占满屏幕宽度
+    const height = 240 / 750 * windowWidth; // 240rpx转换为px,与CSS高度匹配
+    resolve({ width, height });
+  });
+}
+
 function formatPickerDate(d: Date) {
 function formatPickerDate(d: Date) {
-  // 返回 yyyy-MM-dd,用于 picker 的 value
   const y = d.getFullYear()
   const y = d.getFullYear()
   const m = String(d.getMonth() + 1).padStart(2, '0')
   const m = String(d.getMonth() + 1).padStart(2, '0')
   const day = String(d.getDate()).padStart(2, '0')
   const day = String(d.getDate()).padStart(2, '0')
@@ -103,24 +114,44 @@ function formatPickerDate(d: Date) {
 const displayYear = computed(() => current.value.getFullYear())
 const displayYear = computed(() => current.value.getFullYear())
 const displayMonth = computed(() => current.value.getMonth() + 1)
 const displayMonth = computed(() => current.value.getMonth() + 1)
 
 
-// 模拟数据:默认展示本月数据
 const records = ref<RecordItem[]>(generateMockRecords(current.value))
 const records = ref<RecordItem[]>(generateMockRecords(current.value))
 
 
 function generateMockRecords(d: Date): RecordItem[] {
 function generateMockRecords(d: Date): RecordItem[] {
   const y = d.getFullYear()
   const y = d.getFullYear()
   const m = d.getMonth()
   const m = d.getMonth()
   const arr: RecordItem[] = []
   const arr: RecordItem[] = []
-  // 随机生成 0-6 条数据
   const n = Math.floor(Math.random() * 7)
   const n = Math.floor(Math.random() * 7)
   for (let i = 0; i < n; i++) {
   for (let i = 0; i < n; i++) {
     const day = Math.max(1, Math.floor(Math.random() * 28) + 1)
     const day = Math.max(1, Math.floor(Math.random() * 28) + 1)
     const date = new Date(y, m, day)
     const date = new Date(y, m, day)
-    arr.push({ id: `${y}${m}${i}${Date.now()}`, date: formatDisplayDate(date), height: 150 + Math.floor(Math.random() * 50) })
+    arr.push({ 
+      id: `${y}${m}${i}${Date.now()}`, 
+      date: formatDisplayDate(date), 
+      height: 150 + Math.floor(Math.random() * 50)
+    })
   }
   }
-  // 按日期排序
   return arr.sort((a, b) => (a.date < b.date ? 1 : -1))
   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
+}
+
 function formatDisplayDate(d: Date) {
 function formatDisplayDate(d: Date) {
   return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
   return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
 }
 }
@@ -135,330 +166,457 @@ function daysInMonth(year: number, month: number) {
   return new Date(year, month + 1, 0).getDate()
   return new Date(year, month + 1, 0).getDate()
 }
 }
 
 
-// 生成用于绘制折线图的数据点(坐标)
-const pointCoords = computed(() => {
+// Canvas / uCharts 绘图 - 修复版本
+const chartInstance = ref<any>(null)
+const vm = getCurrentInstance()
+let chartInitialized = false
+let chartBusy = false // 绘图锁,防止并发初始化/更新
+
+// 简化的图表绘制函数
+async function drawChart() {
+  // 防止并发调用
+  if (chartBusy) return
+  chartBusy = true
+
+  // 防止重复初始化(已初始化则更新数据)
+  if (chartInitialized && chartInstance.value) {
+    try {
+      await updateChartData()
+    } finally {
+      chartBusy = false
+    }
+    return
+  }
+
+  // 清理旧实例
+  if (chartInstance.value) {
+    try {
+      if (chartInstance.value.destroy) {
+        chartInstance.value.destroy()
+      }
+    } catch (e) {
+      console.warn('Destroy chart error:', e)
+    }
+    chartInstance.value = null
+  }
+
+  if (typeof uCharts === 'undefined') {
+    console.warn('uCharts not available')
+    return
+  }
+
+  // 动态获取 Canvas 容器的宽高 (单位: px)
+  const size = await getCanvasSize();
+  const cssWidth = size.width;
+  const cssHeight = size.height;
+
+  // 获取可靠的设备像素比 - 固定为1避免高分辨率设备上元素过大
+  const pixelRatio = 1; // 关键修复:固定pixelRatio为1
+
+  // 为避免 X 轴标签或绘图区域右侧溢出,保留右侧间距,让绘图区域略窄于 canvas
+  const rightGap = Math.max(24, Math.round(cssWidth * 0.04)) // 最小 24px 或 4% 屏宽
+  const chartWidth = Math.max(cssWidth - rightGap, Math.round(cssWidth * 0.85))
+
+  console.log('Canvas 尺寸与像素比:', { cssWidth, cssHeight, pixelRatio });
+
   const year = current.value.getFullYear()
   const year = current.value.getFullYear()
   const month = current.value.getMonth()
   const month = current.value.getMonth()
   const days = daysInMonth(year, month)
   const days = daysInMonth(year, month)
-  // 宽度 340,高度 120,给上下留白
-  const W = 340
-  const H = 120
-  const leftPad = 10
-  const rightPad = 10
-  const topPad = 10
-  const bottomPad = 10
-  const innerW = W - leftPad - rightPad
-  const innerH = H - topPad - bottomPad
-
-  // 按日填充值(null 表示无数据)
-  const values: Array<number | null> = Array(days).fill(null)
-  records.value.forEach(r => {
+  
+  // 生成合理的categories - 只显示关键日期
+  const categories: string[] = []
+  const showLabelDays = []
+  
+  // 选择要显示的标签:1号、中间几天、最后一天
+  if (days > 0) {
+    showLabelDays.push(1) // 第1天
+    if (days > 1) showLabelDays.push(days) // 最后一天
+    // 中间添加2-3个关键点
+    if (days > 7) showLabelDays.push(Math.ceil(days / 3))
+    if (days > 14) showLabelDays.push(Math.ceil(days * 2 / 3))
+  }
+  
+  for (let d = 1; d <= days; d++) {
+    if (showLabelDays.includes(d)) {
+      categories.push(`${d}日`)
+    } else {
+      categories.push('')
+    }
+  }
+
+  // 只为有记录的天生成categories和data,避免将无数据天设为0
+  const data: number[] = []
+  const filteredCategories: string[] = []
+  // 使用Map按日聚合,保留最新记录(records 数组头部为最新)
+  const dayMap = new Map<number, RecordItem>()
+  for (const r of records.value) {
     const parts = r.date.split('-')
     const parts = r.date.split('-')
     if (parts.length >= 3) {
     if (parts.length >= 3) {
       const y = parseInt(parts[0], 10)
       const y = parseInt(parts[0], 10)
       const m = parseInt(parts[1], 10) - 1
       const m = parseInt(parts[1], 10) - 1
       const d = parseInt(parts[2], 10)
       const d = parseInt(parts[2], 10)
-      if (y === year && m === month && d >= 1 && d <= days) {
-        values[d - 1] = r.height
+      if (y === year && m === month) {
+        // 以最后遍历到的(数组顺序保证头部为最新)作为最终值
+        dayMap.set(d, r)
       }
       }
     }
     }
-  })
+  }
 
 
-  const numeric = values.filter(v => v !== null) as number[]
-  const min = numeric.length ? Math.min(...numeric) : 140
-  const max = numeric.length ? Math.max(...numeric) : 180
-  const range = Math.max(1, max - min)
-
-  // 计算每个点的坐标
-  const coords = values.map((v, idx) => {
-    const x = leftPad + (innerW * idx) / Math.max(1, days - 1)
-    if (v === null) return null
-    const y = topPad + innerH - ((v - min) / range) * innerH
-    return { x, y }
-  })
-  return coords
-})
+  // 将有数据的日期按日顺序输出
+  const sortedDays = Array.from(dayMap.keys()).sort((a, b) => a - b)
+  for (const d of sortedDays) {
+    const rec = dayMap.get(d)!
+    filteredCategories.push(`${d}日`)
+    data.push(rec.height)
+  }
 
 
-const polylinePoints = computed(() => {
-  const pts = pointCoords.value
-  const arr: string[] = []
-  pts.forEach(p => {
-    if (p) arr.push(`${p.x},${p.y}`)
-  })
-  return arr.length ? arr.join(' ') : ''
-})
+  // 如果没有任何数据,则回退为整月空数据(避免 uCharts 抛错)
+  const categoriesToUse = filteredCategories.length ? filteredCategories : categories
 
 
-const areaPoints = computed(() => {
-  const pts = pointCoords.value
-  if (!pts.length) return ''
-  const arr: string[] = []
-  pts.forEach(p => {
-    if (p) arr.push(`${p.x},${p.y}`)
-  })
-  // 加上底部闭合
-  if (arr.length) {
-    const firstX = pts.find(p => p)?.x ?? 0
-    const lastX = pts.slice().reverse().find(p => p)?.x ?? 0
-    const baseY = 120 - 10
-    return `${firstX},${baseY} ` + arr.join(' ') + ` ${lastX},${baseY}`
-  }
-  return ''
-})
+  // 计算合理的Y轴范围
+  const validData = data.filter(v => v > 0)
+  const minVal = validData.length ? Math.floor(Math.min(...validData)) - 2 : 140
+  const maxVal = validData.length ? Math.ceil(Math.max(...validData)) + 2 : 200
 
 
-// Canvas 绘图
-import { onMounted, watch, nextTick, onBeforeUnmount, ref as vueRef, getCurrentInstance } from 'vue'
-const chartCanvas = vueRef<HTMLCanvasElement | null>(null)
-const vm = getCurrentInstance()
+  const series = [{
+    name: '身高',
+    data: data,
+    color: '#ff6a00'
+  }]
 
 
-function getCanvasSize(): Promise<{ width: number; height: number }> {
-  return new Promise(resolve => {
+  // 获取canvas上下文
+  let ctx: any = null
+  try {
+    if (typeof uni !== 'undefined' && typeof uni.createCanvasContext === 'function') {
+      // 小程序环境:优先尝试传入组件实例
+      try {
+        ctx = vm?.proxy ? uni.createCanvasContext('heightChart', vm.proxy) : uni.createCanvasContext('heightChart')
+      } catch (e) {
+        // 再尝试不传vm
+        try { ctx = uni.createCanvasContext('heightChart') } catch (err) { ctx = null }
+      }
+    }
+  } catch (e) {
+    ctx = null
+  }
+
+  // H5环境尝试使用DOM获取2D上下文
+  if (!ctx && typeof document !== 'undefined') {
     try {
     try {
-      if (typeof uni !== 'undefined' && uni.createSelectorQuery) {
-        const query = uni.createSelectorQuery().in(vm?.proxy)
-        query.select('#chartCanvas').boundingClientRect((rect: any) => {
-          if (rect) resolve({ width: rect.width, height: rect.height })
-          else resolve({ width: 340, height: 120 })
-        }).exec()
-      } else if (chartCanvas.value) {
-        resolve({ width: chartCanvas.value.clientWidth || 340, height: chartCanvas.value.clientHeight || 120 })
-      } else {
-        resolve({ width: 340, height: 120 })
+      // 重试逻辑:初次可能未渲染到 DOM
+      let el: HTMLCanvasElement | null = null
+      for (let attempt = 0; attempt < 3; attempt++) {
+        el = document.getElementById('heightChart') as HTMLCanvasElement | null
+        if (el) break
+        // 短延迟后重试(非阻塞)
+        await new Promise(r => setTimeout(r, 50))
+      }
+      
+      if (el && el.getContext) {
+        // Ensure canvas actual pixel size matches cssWidth * pixelRatio
+        try {
+          const physicalW = Math.floor(cssWidth * pixelRatio)
+          const physicalH = Math.floor(cssHeight * pixelRatio)
+          if (el.width !== physicalW || el.height !== physicalH) {
+            el.width = physicalW
+            el.height = physicalH
+            // also adjust style to keep layout consistent
+            el.style.width = cssWidth + 'px'
+            el.style.height = cssHeight + 'px'
+          }
+        } catch (e) {
+          console.warn('Set canvas physical size failed', e)
+        }
+        ctx = el.getContext('2d')
       }
       }
     } catch (e) {
     } catch (e) {
-      resolve({ width: 340, height: 120 })
+      ctx = null
     }
     }
+  }
+
+  if (!ctx) {
+    console.warn('Unable to obtain canvas context for uCharts. Ensure canvas-id matches and vm proxy is available on mini-program.')
+    return
+  }
+
+  console.log('Canvas config:', {
+    width: cssWidth,
+    height: cssHeight,
+    pixelRatio,
+    categoriesLength: categories.length,
+    dataPoints: data.length
   })
   })
-}
 
 
-async function drawChart() {
-  // If running in H5 and canvas DOM is available, use native canvas
-  const canvas = chartCanvas.value as HTMLCanvasElement | null
-  if (canvas && canvas.getContext) {
-    const ctx = canvas.getContext('2d')
-    if (!ctx) return
-    const dpr = (typeof uni !== 'undefined' && uni.getSystemInfoSync) ? (uni.getSystemInfoSync().pixelRatio || (window?.devicePixelRatio || 1)) : (window?.devicePixelRatio || 1)
-    const cssWidth = canvas.clientWidth || 340
-    const cssHeight = canvas.clientHeight || 120
-    canvas.width = Math.round(cssWidth * dpr)
-    canvas.height = Math.round(cssHeight * dpr)
-    ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
-    ctx.clearRect(0, 0, cssWidth, cssHeight)
-    ctx.strokeStyle = 'rgba(0,0,0,0.04)'
-    ctx.lineWidth = 1
-    for (let i = 0; i <= 4; i++) {
-      const y = (cssHeight / 4) * i
-      ctx.beginPath()
-      ctx.moveTo(0, y)
-      ctx.lineTo(cssWidth, y)
-      ctx.stroke()
-    }
-    const scaleX = cssWidth / 340
-    const scaleY = cssHeight / 120
-    const pts = pointCoords.value.map(p => p ? { x: p.x * scaleX, y: p.y * scaleY } : null)
-    const validPts = pts.filter(p => p !== null) as { x: number; y: number }[]
-
-    // draw Y axis labels (4 ticks) and X axis day labels
-    const year = current.value.getFullYear()
-    const month = current.value.getMonth()
-    const days = daysInMonth(year, month)
-    const valuesForTicks: Array<number | null> = Array(days).fill(null)
-    records.value.forEach(r => {
-      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 && d >= 1 && d <= days) valuesForTicks[d - 1] = r.height
+  // 简化的uCharts配置 - 关闭所有可能产生重叠的选项
+  const config = {
+    $this: vm?.proxy,
+    canvasId: 'heightChart',
+    context: ctx,
+    type: 'line',
+    fontSize: 10, // 全局字体大小,参考微信小程序示例
+    categories: categoriesToUse,
+    series: series,
+    // 使用比 canvas 略窄的绘图宽度并设置 padding 来避免右边溢出
+    width: chartWidth,
+    padding: [10, rightGap + 8, 18, 10],
+    height: cssHeight,
+    pixelRatio: pixelRatio,
+    background: 'transparent',
+    animation: false, // 关闭动画避免干扰
+    enableScroll: false,
+    dataLabel: false, // 关键:关闭数据点标签
+    legend: {
+      show: false
+    },
+    xAxis: {
+      disableGrid: true, // 简化网格
+      axisLine: true,
+      axisLineColor: '#e0e0e0',
+      fontColor: '#666666',
+      fontSize: 10, // 进一步调小X轴字体
+      boundaryGap: 'justify'
+    },
+    yAxis: {
+      disableGrid: false,
+      gridColor: '#f5f5f5',
+      splitNumber: 4, // 减少分割数
+      min: minVal,
+      max: maxVal,
+      axisLine: true,
+      axisLineColor: '#e0e0e0',
+      fontColor: '#666666',
+      fontSize: 10, // 进一步调小Y轴字体
+      format: (val: number) => val % 1 === 0 ? `${val}cm` : '' // 只显示整数值
+    },
+    extra: {
+      line: {
+        type: 'curve',
+        width: 1, // 进一步调细线宽
+        activeType: 'point', // 简化点样式
+        point: {
+          radius: 0.5, // 进一步调小数据点半径
+          strokeWidth: 0.5 // 调小边框宽度
+        }
+      },
+      tooltip: {
+        showBox: false, // 关闭提示框避免重叠
+        showCategory: false
       }
       }
-    })
-    const numeric = valuesForTicks.filter(v => v !== null) as number[]
-    const minVal = numeric.length ? Math.min(...numeric) : 140
-    const maxVal = numeric.length ? Math.max(...numeric) : 180
-    const range = Math.max(1, maxVal - minVal)
-    const leftPad = 10 * scaleX
-    const topPad = 10 * scaleY
-    const innerW = (340 - 10 - 10) * scaleX
-    const innerH = (120 - 10 - 10) * scaleY
-    ctx.fillStyle = '#999'
-    ctx.font = '12px sans-serif'
-    ctx.textAlign = 'right'
-    ctx.textBaseline = 'middle'
-    for (let j = 0; j <= 4; j++) {
-      const val = Math.round((maxVal - (range * j) / 4) * 10) / 10
-      const y = topPad + (innerH * j) / 4
-      ctx.fillText(String(val), leftPad - 6, y)
-      ctx.beginPath()
-      ctx.moveTo(leftPad - 3, y)
-      ctx.lineTo(leftPad, y)
-      ctx.strokeStyle = 'rgba(0,0,0,0.08)'
-      ctx.stroke()
-    }
-    const step = Math.max(1, Math.ceil(days / 6))
-    ctx.textAlign = 'center'
-    ctx.textBaseline = 'top'
-    for (let d = 1; d <= days; d += step) {
-      const x = leftPad + (innerW * (d - 1)) / Math.max(1, days - 1)
-      const yTick = topPad + innerH
-      ctx.beginPath()
-      ctx.moveTo(x, yTick)
-      ctx.lineTo(x, yTick + 6)
-      ctx.strokeStyle = 'rgba(0,0,0,0.08)'
-      ctx.stroke()
-      ctx.fillText(String(d), x, yTick + 8)
     }
     }
-    if (validPts.length) {
-      ctx.beginPath()
-      ctx.moveTo(validPts[0].x, validPts[0].y)
-      validPts.forEach(p => ctx.lineTo(p.x, p.y))
-      ctx.lineTo(validPts[validPts.length - 1].x, cssHeight)
-      ctx.lineTo(validPts[0].x, cssHeight)
-      ctx.closePath()
-      ctx.fillStyle = 'rgba(255,106,0,0.08)'
-      ctx.fill()
+  }
+
+  try {
+    // 在创建新实例前确保销毁旧实例
+    if (chartInstance.value && chartInstance.value.destroy) {
+      try { chartInstance.value.destroy() } catch (e) { console.warn('destroy before init failed', e) }
+      chartInstance.value = null
+      chartInitialized = false
     }
     }
-    if (validPts.length) {
-      ctx.beginPath()
-      ctx.moveTo(validPts[0].x, validPts[0].y)
-      validPts.forEach(p => ctx.lineTo(p.x, p.y))
-      ctx.strokeStyle = '#ff6a00'
-      ctx.lineWidth = 2
-      ctx.lineJoin = 'round'
-      ctx.lineCap = 'round'
-      ctx.stroke()
+
+    chartInstance.value = new uCharts(config)
+    chartInitialized = true
+    console.log('uCharts initialized successfully')
+  } catch (error) {
+    console.error('uCharts init error:', error)
+    chartInitialized = false
+  }
+  chartBusy = false
+}
+
+// 更新数据而不重新初始化
+async function updateChartData() {
+  if (chartBusy) return
+  chartBusy = true
+
+  if (!chartInstance.value || !chartInitialized) {
+    try {
+      await drawChart()
+    } finally {
+      chartBusy = false
     }
     }
-    validPts.forEach(p => {
-      ctx.beginPath()
-      ctx.fillStyle = '#ff6a00'
-      ctx.arc(p.x, p.y, 3, 0, Math.PI * 2)
-      ctx.fill()
-    })
     return
     return
   }
   }
 
 
-  // Otherwise fallback to uni.createCanvasContext for mini-programs / native canvases
-  try {
-    const size = await getCanvasSize()
-    const cssWidth = size.width || 340
-    const cssHeight = size.height || 120
-    const ctx = (typeof uni !== 'undefined' && uni.createCanvasContext) ? uni.createCanvasContext('heightChart', vm?.proxy) : null
-    if (!ctx) return
-    // clear by drawing a transparent rect
-    ctx.clearRect && ctx.clearRect(0, 0, cssWidth, cssHeight)
-    ctx.setStrokeStyle && ctx.setStrokeStyle('rgba(0,0,0,0.04)')
-    for (let i = 0; i <= 4; i++) {
-      const y = (cssHeight / 4) * i
-      if (ctx.beginPath) ctx.beginPath()
-      ctx.moveTo && ctx.moveTo(0, y)
-      ctx.lineTo && ctx.lineTo(cssWidth, y)
-      ctx.stroke && ctx.stroke()
+  const year = current.value.getFullYear()
+  const month = current.value.getMonth()
+  const days = daysInMonth(year, month)
+  
+  const categories: string[] = []
+  const showLabelDays = []
+  
+  if (days > 0) {
+    showLabelDays.push(1)
+    if (days > 1) showLabelDays.push(days)
+    if (days > 7) showLabelDays.push(Math.ceil(days / 3))
+    if (days > 14) showLabelDays.push(Math.ceil(days * 2 / 3))
+  }
+  
+  for (let d = 1; d <= days; d++) {
+    if (showLabelDays.includes(d)) {
+      categories.push(`${d}日`)
+    } else {
+      categories.push('')
     }
     }
-    const scaleX = cssWidth / 340
-    const scaleY = cssHeight / 120
-    const pts = pointCoords.value.map(p => p ? { x: p.x * scaleX, y: p.y * scaleY } : null)
-    const validPts = pts.filter(p => p !== null) as { x: number; y: number }[]
-    // draw labels (mini-program canvas API)
-    const year = current.value.getFullYear()
-    const month = current.value.getMonth()
-    const days = daysInMonth(year, month)
-    const valuesForTicks: Array<number | null> = Array(days).fill(null)
-    records.value.forEach(r => {
-      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 && d >= 1 && d <= days) valuesForTicks[d - 1] = r.height
+  }
+
+  // 只为有记录的天生成categories和data
+  const data: number[] = []
+  const filteredCategories: string[] = []
+  const dayMap = new Map<number, RecordItem>()
+  for (const r of records.value) {
+    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) {
+        dayMap.set(d, r)
       }
       }
-    })
-    const numeric = valuesForTicks.filter(v => v !== null) as number[]
-    const minVal = numeric.length ? Math.min(...numeric) : 140
-    const maxVal = numeric.length ? Math.max(...numeric) : 180
-    const range = Math.max(1, maxVal - minVal)
-    const leftPad = 10 * scaleX
-    const topPad = 10 * scaleY
-    const innerW = (340 - 10 - 10) * scaleX
-    const innerH = (120 - 10 - 10) * scaleY
-    ctx.setFillStyle && ctx.setFillStyle('#999')
-    ctx.setFontSize && ctx.setFontSize(12)
-    // Y axis
-    for (let j = 0; j <= 4; j++) {
-      const val = Math.round((maxVal - (range * j) / 4) * 10) / 10
-      const y = topPad + (innerH * j) / 4
-      ctx.fillText && ctx.fillText(String(val), leftPad - 6, y)
-      ctx.beginPath && ctx.beginPath()
-      ctx.moveTo && ctx.moveTo(leftPad - 3, y)
-      ctx.lineTo && ctx.lineTo(leftPad, y)
-      ctx.stroke && ctx.stroke()
-    }
-    const step = Math.max(1, Math.ceil(days / 6))
-    // X axis
-    for (let d = 1; d <= days; d += step) {
-      const x = leftPad + (innerW * (d - 1)) / Math.max(1, days - 1)
-      const yTick = topPad + innerH
-      ctx.moveTo && ctx.moveTo(x, yTick)
-      ctx.lineTo && ctx.lineTo(x, yTick + 6)
-      ctx.stroke && ctx.stroke()
-      ctx.fillText && ctx.fillText(String(d), x, yTick + 8)
     }
     }
-    if (validPts.length) {
-      ctx.beginPath && ctx.beginPath()
-      ctx.moveTo && ctx.moveTo(validPts[0].x, validPts[0].y)
-      validPts.forEach(p => ctx.lineTo && ctx.lineTo(p.x, p.y))
-      ctx.lineTo && ctx.lineTo(validPts[validPts.length - 1].x, cssHeight)
-      ctx.lineTo && ctx.lineTo(validPts[0].x, cssHeight)
-      ctx.closePath && ctx.closePath()
-      ctx.setFillStyle && ctx.setFillStyle('rgba(255,106,0,0.08)')
-      ctx.fill && ctx.fill()
-    }
-    if (validPts.length) {
-      ctx.beginPath && ctx.beginPath()
-      ctx.moveTo && ctx.moveTo(validPts[0].x, validPts[0].y)
-      validPts.forEach(p => ctx.lineTo && ctx.lineTo(p.x, p.y))
-      ctx.setStrokeStyle && ctx.setStrokeStyle('#ff6a00')
-      ctx.setLineWidth && ctx.setLineWidth(2)
-      ctx.stroke && ctx.stroke()
+  }
+  const sortedDays = Array.from(dayMap.keys()).sort((a, b) => a - b)
+  for (const d of sortedDays) {
+    const rec = dayMap.get(d)!
+    filteredCategories.push(`${d}日`)
+    data.push(rec.height)
+  }
+  const categoriesToUse = filteredCategories.length ? filteredCategories : categories
+
+  const validData = data.filter(v => v > 0)
+  const minVal = validData.length ? Math.floor(Math.min(...validData)) - 2 : 140
+  const maxVal = validData.length ? Math.ceil(Math.max(...validData)) + 2 : 200
+
+  try {
+    // 使用uCharts的更新方法,仅更新有数据的分类和序列
+    // 若实例存在,先更新宽度/padding(如果有配置)以避免右侧溢出
+    try {
+      const size = await getCanvasSize()
+      const cssWidth = size.width
+      const rightGap = Math.max(24, Math.round(cssWidth * 0.04))
+      const chartWidth = Math.max(cssWidth - rightGap, Math.round(cssWidth * 0.85))
+      if (chartInstance.value.opts) {
+        chartInstance.value.opts.width = chartWidth
+        chartInstance.value.opts.padding = [10, rightGap + 8, 18, 10]
+      }
+    } catch (e) {
+      // 忽略尺寸更新错误
     }
     }
-    validPts.forEach(p => {
-      ctx.beginPath && ctx.beginPath()
-      ctx.arc && ctx.arc(p.x, p.y, 3, 0, Math.PI * 2)
-      ctx.setFillStyle && ctx.setFillStyle('#ff6a00')
-      ctx.fill && ctx.fill()
+
+    chartInstance.value.updateData({
+      categories: categoriesToUse,
+      series: [{
+        name: '身高',
+        data: data,
+        color: '#ff6a00'
+      }]
     })
     })
-    ctx.draw && ctx.draw(true)
-  } catch (err) {
-    // ignore
+
+    // 更新Y轴范围
+    chartInstance.value.opts.yAxis.min = minVal
+    chartInstance.value.opts.yAxis.max = maxVal
+
+  } catch (error) {
+    console.error('Update chart error:', error)
+    // 如果更新失败,重新销毁并重建实例
+    try {
+      if (chartInstance.value && chartInstance.value.destroy) chartInstance.value.destroy()
+    } catch (e) { console.warn('destroy on update failure failed', e) }
+    chartInstance.value = null
+    chartInitialized = false
+    try {
+      await drawChart()
+    } catch (e) { console.error('re-init after update failure also failed', e) }
   }
   }
+  chartBusy = false
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
-  nextTick(() => drawChart())
+  // 延迟确保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 drawChart()
+  }, 500)
+})
+
+// 简化监听,避免频繁重绘
+watch([() => current.value], async () => {
+  setTimeout(async () => {
+    await updateChartData()
+  }, 100)
 })
 })
 
 
-watch([() => records.value, () => current.value], () => {
-  nextTick(() => drawChart())
+watch([() => records.value], async () => {
+  setTimeout(async () => {
+    await updateChartData()
+  }, 100)
 }, { deep: true })
 }, { deep: true })
 
 
 onBeforeUnmount(() => {
 onBeforeUnmount(() => {
-  // no special cleanup required
+  if (chartInstance.value && chartInstance.value.destroy) {
+    try {
+      chartInstance.value.destroy()
+    } catch (e) {
+      console.warn('uCharts destroy error:', e)
+    }
+  }
+  chartInstance.value = null
+  chartInitialized = false
 })
 })
 
 
-function prevMonth() {
+// 强制重建图表(用于切换月份时彻底刷新,如同退出页面再进入)
+async function rebuildChart() {
+  // 如果正在绘制,等一小会儿再销毁
+  if (chartBusy) {
+    // 等待最大 300ms,避免长时间阻塞
+    await new Promise(r => setTimeout(r, 50))
+  }
+  try {
+    if (chartInstance.value && chartInstance.value.destroy) {
+      try { chartInstance.value.destroy() } catch (e) { console.warn('destroy in rebuildChart failed', e) }
+    }
+  } catch (e) {
+    console.warn('rebuildChart destroy error', e)
+  }
+  chartInstance.value = null
+  chartInitialized = false
+  // 等待 DOM/Tick 稳定
+  await nextTick()
+  // 重新初始化
+  try {
+    await drawChart()
+  } catch (e) {
+    console.error('rebuildChart drawChart failed', e)
+  }
+}
+
+// 其他函数保持不变
+async function prevMonth() {
   const d = new Date(current.value)
   const d = new Date(current.value)
   d.setMonth(d.getMonth() - 1)
   d.setMonth(d.getMonth() - 1)
   current.value = d
   current.value = d
   pickerValue.value = formatPickerDate(d)
   pickerValue.value = formatPickerDate(d)
   records.value = generateMockRecords(d)
   records.value = generateMockRecords(d)
+  await rebuildChart()
 }
 }
 
 
-function nextMonth() {
+async function nextMonth() {
   const d = new Date(current.value)
   const d = new Date(current.value)
   d.setMonth(d.getMonth() + 1)
   d.setMonth(d.getMonth() + 1)
   current.value = d
   current.value = d
   pickerValue.value = formatPickerDate(d)
   pickerValue.value = formatPickerDate(d)
   records.value = generateMockRecords(d)
   records.value = generateMockRecords(d)
+  await rebuildChart()
 }
 }
 
 
-function onPickerChange(e: any) {
+async function onPickerChange(e: any) {
   const val = e?.detail?.value || e
   const val = e?.detail?.value || e
   const parts = (val as string).split('-')
   const parts = (val as string).split('-')
   if (parts.length >= 2) {
   if (parts.length >= 2) {
@@ -468,31 +626,29 @@ function onPickerChange(e: any) {
     current.value = d
     current.value = d
     pickerValue.value = formatPickerDate(d)
     pickerValue.value = formatPickerDate(d)
     records.value = generateMockRecords(d)
     records.value = generateMockRecords(d)
+    await rebuildChart()
   }
   }
 }
 }
 
 
-// 添加逻辑
+// 添加逻辑保持不变
 const showAdd = ref(false)
 const showAdd = ref(false)
 const addDate = ref(formatPickerDate(new Date()))
 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)
 
 
-// 刻度尺(添加模态使用)的联动处理
 function onAddRulerUpdate(val: number) {
 function onAddRulerUpdate(val: number) {
-  addHeight.value = val
-  // 实时打印用户滑动时的刻度(update 在滚动过程中会频繁触发)
-  console.log('[ScaleRuler] add update:value ->', val)
+  addHeight.value = Number(val.toFixed(1))
 }
 }
+
 function onAddRulerChange(val: number) {
 function onAddRulerChange(val: number) {
-  addHeight.value = val
-  console.log('[ScaleRuler] add change ->', val)
+  addHeight.value = Number(val.toFixed(1))
 }
 }
 
 
 function openAdd() {
 function openAdd() {
   showAdd.value = true
   showAdd.value = true
-  // 打开添加模态时,初始化刻度尺/输入默认值(若之前无值则设为 170)
   if (!addHeight.value) addHeight.value = 170
   if (!addHeight.value) addHeight.value = 170
 }
 }
+
 function closeAdd() {
 function closeAdd() {
   showAdd.value = false
   showAdd.value = false
   addHeight.value = null
   addHeight.value = null
@@ -501,282 +657,293 @@ function closeAdd() {
 function onAddDateChange(e: any) {
 function onAddDateChange(e: any) {
   const val = e?.detail?.value || e
   const val = e?.detail?.value || e
   addDate.value = val
   addDate.value = val
-  addDateLabel.value = val.replace(/^(\d{4})-(\d{2})-(\d{2}).*$/, '$1-$2-$3')
+  addDateLabel.value = val.replace(/^(.{10}).*$/, '$1')
 }
 }
 
 
-function confirmAdd() {
+async function confirmAdd() {
   if (!addHeight.value) {
   if (!addHeight.value) {
-    uni.showToast({ title: '请输入身高', icon: 'none' })
+    uni.showToast && uni.showToast({ title: '请输入身高', icon: 'none' })
     return
     return
   }
   }
   const id = `user-${Date.now()}`
   const id = `user-${Date.now()}`
-  const item: RecordItem = { id, date: addDateLabel.value, height: addHeight.value }
-  // 如果添加的月份与当前展示月份一致,插入
+  const item: RecordItem = { id, date: addDateLabel.value, height: Number(Number(addHeight.value).toFixed(1)) }
   const parts = addDate.value.split('-')
   const parts = addDate.value.split('-')
   const addY = parseInt(parts[0], 10)
   const addY = parseInt(parts[0], 10)
   const addM = parseInt(parts[1], 10) - 1
   const addM = parseInt(parts[1], 10) - 1
   if (addY === current.value.getFullYear() && addM === current.value.getMonth()) {
   if (addY === current.value.getFullYear() && addM === current.value.getMonth()) {
     records.value = [item, ...records.value]
     records.value = [item, ...records.value]
   }
   }
-  uni.showToast({ title: '已添加', icon: 'success' })
+  uni.showToast && uni.showToast({ title: '已添加', icon: 'success' })
   closeAdd()
   closeAdd()
+  // 新增记录后彻底重建图表,确保像退出再进入一样刷新
+  try {
+    await rebuildChart()
+  } catch (e) {
+    console.warn('rebuildChart after add failed', e)
+  }
 }
 }
 
 
-// 删除记录逻辑:弹出确认对话框,确认后从 records 中移除
-function confirmDeleteRecord(id: string) {
+async function confirmDeleteRecord(id: string) {
   if (typeof uni !== 'undefined' && uni.showModal) {
   if (typeof uni !== 'undefined' && uni.showModal) {
-    uni.showModal({
-      title: '删除记录',
-      content: '确认要删除该条身高记录吗?此操作无法撤销。',
-      confirmText: '删除',
-      cancelText: '取消',
-      success: (res: any) => {
+    uni.showModal({ 
+      title: '删除', 
+      content: '确认删除该条记录吗?', 
+      success: async (res: any) => { 
         if (res.confirm) {
         if (res.confirm) {
           records.value = records.value.filter(r => r.id !== id)
           records.value = records.value.filter(r => r.id !== id)
-          uni.showToast({ title: '已删除', icon: 'success' })
+          try { await rebuildChart() } catch (e) { console.warn('rebuildChart after delete failed', e) }
         }
         }
       }
       }
     })
     })
   } else {
   } else {
-    // Fallback:直接删除
     records.value = records.value.filter(r => r.id !== id)
     records.value = records.value.filter(r => r.id !== id)
+    try { await rebuildChart() } catch (e) { console.warn('rebuildChart after delete failed', e) }
   }
   }
 }
 }
-
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
-.page {
-  min-height: calc(100vh);
-  padding-top: calc(var(--status-bar-height) + 44px);
-  background: #f5f6f8;
-  box-sizing: border-box
+.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 
 }
 }
 
 
-.header {
-  padding: 20rpx 40rpx
+.month-label { 
+  font-size: 34rpx; 
+  color: #333 
 }
 }
 
 
-.month-selector {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 12rpx
+.btn { 
+  background: transparent; 
+  border: none; 
+  font-size: 36rpx; 
+  color: #666 
 }
 }
 
 
-.month-label {
-  font-size: 34rpx;
-  color: #333
+.content { 
+  padding: 20rpx 24rpx 100rpx 24rpx 
 }
 }
 
 
-.btn {
-  background: transparent;
-  border: none;
-  font-size: 36rpx;
-  color: #666
+.chart-wrap { 
+  background: #fff; 
+  border-radius: 12rpx; 
+  padding: 24rpx; 
+  margin: 0 24rpx 20rpx 24rpx; 
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03) 
 }
 }
 
 
-.content {
-  padding: 20rpx 24rpx 100rpx 24rpx
+.chart-header { 
+  font-size: 32rpx; 
+  color: #333; 
+  margin-bottom: 20rpx; 
+  font-weight: 600 
 }
 }
 
 
-.chart-wrap {
-  padding: 12rpx 24rpx
+/* 关键修复:确保canvas样式正确,参考微信小程序示例 */
+.chart-canvas {
+  width: 750rpx;
+  height: 240rpx;
+  background-color: #FFFFFF;
+  display: block;
 }
 }
 
 
-.chart-header {
-  font-size: 28rpx;
-  color: #666;
-  margin-bottom: 10rpx
+.summary { 
+  padding: 20rpx; 
+  color: #666; 
+  font-size: 28rpx 
 }
 }
 
 
-.chart-svg {
-  width: 100%;
-  height: 160rpx;
-  display: block
+.list { 
+  background: #fff; 
+  border-radius: 12rpx; 
+  padding: 10rpx; 
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03) 
 }
 }
 
 
-.summary {
-  padding: 20rpx;
-  color: #666;
-  font-size: 28rpx
+.empty { 
+  padding: 40rpx; 
+  text-align: center; 
+  color: #999 
 }
 }
 
 
-.list {
-  background: #fff;
-  border-radius: 12rpx;
-  padding: 10rpx;
-  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03)
+.list-item { 
+  display: flex; 
+  align-items: center; 
+  padding: 20rpx; 
+  border-bottom: 1rpx solid #f0f0f0 
 }
 }
 
 
-.empty {
-  padding: 40rpx;
-  text-align: center;
-  color: #999
+.list-item .date { 
+  color: #666 
 }
 }
 
 
-.list-item {
-  display: flex;
-  align-items: center;
-  padding: 20rpx;
-  border-bottom: 1rpx solid #f0f0f0;
+.list-item .value { 
+  color: #333; 
+  font-weight: 600; 
+  flex: 1; 
+  text-align: right 
 }
 }
 
 
-.list-item .date {
-  color: #666
+.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 
 }
 }
 
 
-.list-item .value {
-  color: #333;
-  font-weight: 600;
-  flex: 1; /* allow value to take remaining space */
-  text-align: right; /* keep value left aligned so delete button on right */
+.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 
 }
 }
 
 
-.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-inner { 
+  color: #fff; 
+  font-size: 56rpx; 
+  line-height: 56rpx 
 }
 }
 
 
-.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
+.modal { 
+  position: fixed; 
+  left: 0; 
+  right: 0; 
+  top: 0; 
+  bottom: 0; 
+  display: flex; 
+  align-items: flex-end; 
+  justify-content: center; 
+  z-index: 1300 
 }
 }
 
 
-.fab-inner {
-  color: #fff;
-  font-size: 56rpx;
-  line-height: 56rpx
+.modal-backdrop { 
+  position: absolute; 
+  left: 0; 
+  right: 0; 
+  top: 0; 
+  bottom: 0; 
+  background: rgba(0, 0, 0, 0.4) 
 }
 }
 
 
-.modal {
-  position: fixed;
-  left: 0;
-  right: 0;
-  top: 0;
-  bottom: 0;
-  display: flex;
-  align-items: flex-end;
-  justify-content: center;
-  z-index: 1300
+.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-backdrop {
-  position: absolute;
-  left: 0;
-  right: 0;
-  top: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.4)
+.modal-title { 
+  font-size: 56rpx; 
+  margin-block: 60rpx; 
+  color: #222; 
+  font-weight: 700; 
+  letter-spacing: 1rpx 
 }
 }
 
 
-.modal-panel {
-  position: relative;
-  width: 100%;
-  background: #fff;
-  border-top-left-radius: 18rpx;
-  border-top-right-radius: 18rpx;
-  padding: 28rpx 24rpx 140rpx 24rpx; /* leave space for fixed footer */
-  box-shadow: 0 -8rpx 30rpx rgba(0,0,0,0.12);
-}
+.modal-inner { 
+  max-width: 70%; 
+  margin: 0 auto 
+}
 
 
-.modal-title {
-  font-size: 56rpx;
-  margin-block:  60rpx;
-  color: #222;
-  font-weight: 700;
-  letter-spacing: 1rpx;
-}
+.form-row { 
+  display: flex; 
+  align-items: center; 
+  justify-content: space-between; 
+  margin-bottom: 34rpx; 
+  padding: 14rpx 0; 
+  font-size: 32rpx 
+}
+
+.input { 
+  width: 240rpx; 
+  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) 
+}
 
 
-.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: 240rpx;
-  text-align: right;
-  padding: 16rpx;
-  border-radius: 14rpx;
-  border: 1rpx solid #eee;
-  background: #fff7f0;
-}
-
-.picker-display {
-  color: #333
-}
-
-.modal-actions {
-  display: flex;
-  justify-content: center;
-  gap: 12rpx;
-  margin-top: 18rpx;
-}
-
-.modal-actions.full {
-  padding: 6rpx 0 24rpx 0;
-}
-
-.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 
+}
 
 
-/* Drag handle and header */
-.drag-handle {
-  width: 64rpx;
-  height: 6rpx;
-  background: rgba(0,0,0,0.08);
-  border-radius: 999px;
-  margin: 10rpx auto 14rpx auto;
+.label { 
+  color: #666 
 }
 }
-.modal-header {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 12rpx;
-  margin-bottom: 6rpx;
+
+.ruler-wrap { 
+  margin: 12rpx 0 
 }
 }
-.label {
-  color: #666;
+
+.fixed-footer { 
+  position: absolute; 
+  left: 0; 
+  right: 0; 
+  bottom: 40rpx; 
+  padding: 0 24rpx 
 }
 }
-.ruler-wrap {
-  margin: 12rpx 0;
-  /* keep full width so ScaleRuler spans screen width */
+
+.btn-full { 
+  width: 100%; 
+  padding: 18rpx; 
+  border-radius: 12rpx; 
 }
 }
 </style>
 </style>