Răsfoiți Sursa

feat(health): 血压详情页支持周/月视图切换

- 新增周视图和月视图切换功能
- 使用 multiSelector 优化年月选择器
- 支持按周显示血压数据趋势图
- 调整图表绘制逻辑以适配不同视图模式
- 更新 mock 数据生成逻辑以支持周视图
- 修改 UI 布局以容纳新的控制元素
- 优化图表更新逻辑提高性能
- 添加周数计算和日期处理工具函数
mcbaiyun 2 luni în urmă
părinte
comite
274cee74ec
1 a modificat fișierele cu 276 adăugiri și 295 ștergeri
  1. 276 295
      src/pages/health/details/blood-pressure.vue

+ 276 - 295
src/pages/health/details/blood-pressure.vue

@@ -4,13 +4,18 @@
 
     <view class="header">
       <view class="month-selector">
-        <button class="btn" @click="prevMonth">‹</button>
-        <view class="month-label">{{ displayYear }}年 {{ displayMonth }}月</view>
-        <button class="btn" @click="nextMonth">›</button>
+        <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>
-      <picker mode="date" :value="pickerValue" @change="onPickerChange">
-        <view class="picker-display">切换月份</view>
-      </picker>
     </view>
 
     <!-- 趋势图 - 简化canvas设置 -->
@@ -95,7 +100,17 @@ type RecordItem = { id: string; date: string; s: number; d: number }
 
 // 当前展示年月
 const current = ref(new Date())
-const pickerValue = ref(formatPickerDate(current.value))
+// 使用 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}年`),
+  Array.from({ length: 12 }, (_, i) => `${i + 1}月`)
+])
 
 // 明确的canvas尺寸(将由 getCanvasSize 初始化以匹配设备宽度)
 const canvasWidth = ref(700) // 初始值,会在 mounted 时覆盖
@@ -122,24 +137,43 @@ function formatPickerDate(d: Date) {
 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 y = d.getFullYear()
-  const m = d.getMonth()
   const arr: RecordItem[] = []
-  const n = Math.floor(Math.random() * 7)
-  for (let i = 0; i < n; i++) {
-    const day = Math.max(1, Math.floor(Math.random() * 28) + 1)
-    const date = new Date(y, m, day)
-    const s = 100 + Math.floor(Math.random() * 40)
-    const dval = 60 + Math.floor(Math.random() * 30)
-    arr.push({ 
-      id: `${y}${m}${i}${Date.now()}`, 
-      date: formatDisplayDate(date), 
-      s, 
-      d: dval 
-    })
+  if (viewMode.value === 'month') {
+    const y = d.getFullYear()
+    const m = d.getMonth()
+    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)
+      const s = 100 + Math.floor(Math.random() * 40)
+      const dval = 60 + Math.floor(Math.random() * 30)
+      arr.push({ id: `${date.getTime()}-${i}`, date: formatDisplayDate(date), s, d: dval })
+    }
+  } else {
+    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)
+      const date = new Date(weekStart)
+      date.setDate(weekStart.getDate() + dayOffset)
+      const s = 100 + Math.floor(Math.random() * 40)
+      const dval = 60 + Math.floor(Math.random() * 30)
+      arr.push({ id: `${date.getTime()}-${i}`, date: formatDisplayDate(date), s, d: dval })
+    }
   }
   return arr.sort((a, b) => (a.date < b.date ? 1 : -1))
 }
@@ -182,275 +216,185 @@ function daysInMonth(year: number, month: number) {
   return new Date(year, month + 1, 0).getDate()
 }
 
+// 获取指定日期所在周的开始日期(星期一)
+function getWeekStart(date: Date): Date {
+  const d = new Date(date)
+  d.setHours(0, 0, 0, 0)
+  const day = d.getDay()
+  const diff = day === 0 ? -6 : 1 - day
+  d.setDate(d.getDate() + diff)
+  d.setHours(0, 0, 0, 0)
+  return d
+}
+
+// 获取指定日期所在周的结束日期(星期日)
+function getWeekEnd(date: Date): Date {
+  const d = getWeekStart(date)
+  d.setDate(d.getDate() + 6)
+  d.setHours(0, 0, 0, 0)
+  return d
+}
+
+// 获取指定日期所在周的周数(一年中的第几周)
+function getWeekNumber(date: Date): number {
+  const d = new Date(date)
+  d.setHours(0, 0, 0, 0)
+  d.setDate(d.getDate() + 4 - (d.getDay() || 7))
+  const yearStart = new Date(d.getFullYear(), 0, 1)
+  return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7)
+}
+
 // 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
-    }
+    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)
-    }
+    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;
+  if (typeof uCharts === 'undefined') { console.warn('uCharts not available'); return }
 
-  // 获取可靠的设备像素比 - 固定为1避免高分辨率设备上元素过大
-  const pixelRatio = 1; // 关键修复:固定pixelRatio为1
-
-  // 为避免 X 轴标签或绘图区域右侧溢出,保留右侧间距,让绘图区域略窄于 canvas
-  const rightGap = Math.max(24, Math.round(cssWidth * 0.04)) // 最小 24px 或 4% 屏宽
+  const size = await getCanvasSize()
+  const cssWidth = size.width
+  const cssHeight = size.height
+  const pixelRatio = 1
+  const rightGap = Math.max(24, Math.round(cssWidth * 0.04))
   const chartWidth = Math.max(cssWidth - rightGap, Math.round(cssWidth * 0.85))
 
-  console.log('Canvas 尺寸与像素比:', { cssWidth, cssHeight, pixelRatio });
-
   const year = current.value.getFullYear()
   const month = current.value.getMonth()
-  const days = daysInMonth(year, month)
-  
-  // 生成合理的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('')
+  if (viewMode.value === 'month') {
+    const days = daysInMonth(year, month)
+    const showLabelDays: number[] = []
+    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++) categories.push(showLabelDays.includes(d) ? `${d}日` : '')
+  } else {
+    const weekStart = getWeekStart(current.value)
+    const weekDays = ['一','二','三','四','五','六','日']
+    for (let i = 0; i < 7; i++) {
+      const date = new Date(weekStart)
+      date.setDate(weekStart.getDate() + i)
+      categories.push(`${date.getDate()}日(${weekDays[i]})`)
     }
   }
 
-  // 只为有记录的天生成categories和data,避免将无数据天设为0
   const sData: number[] = []
   const dData: number[] = []
   const filteredCategories: string[] = []
-  // 使用Map按日聚合,保留最新记录(records 数组头部为最新)
   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)
+
+  if (viewMode.value === 'month') {
+    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 sortedDays = Array.from(dayMap.keys()).sort((a, b) => a - b)
+    for (const day of sortedDays) { const rec = dayMap.get(day)!; filteredCategories.push(`${day}日`); sData.push(rec.s); dData.push(rec.d) }
+  } else {
+    const weekStart = getWeekStart(current.value)
+    for (const r of records.value) {
+      const parts = r.date.split('-')
+      if (parts.length >= 3) {
+        const recordDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10))
+        const weekStartDate = getWeekStart(recordDate)
+        if (weekStartDate.getTime() === weekStart.getTime()) {
+          const dayOfWeek = recordDate.getDay() || 7
+          dayMap.set(dayOfWeek, r)
+        }
+      }
+    }
+    const weekDays = ['一','二','三','四','五','六','日']
+    for (let i = 1; i <= 7; i++) {
+      const rec = dayMap.get(i)
+      if (rec) { const date = new Date(rec.date); filteredCategories.push(`${date.getDate()}日(${weekDays[i-1]})`); sData.push(rec.s); dData.push(rec.d) }
+    }
   }
 
-  // 将有数据的日期按日顺序输出
-  const sortedDays = Array.from(dayMap.keys()).sort((a, b) => a - b)
-  for (const day of sortedDays) {
-    const rec = dayMap.get(day)!
-    filteredCategories.push(`${day}日`)
-    sData.push(rec.s)
-    dData.push(rec.d)
-  }
-
-  // 如果没有任何数据,则回退为整月空数据(避免 uCharts 抛错)
   const categoriesToUse = filteredCategories.length ? filteredCategories : categories
 
-  // 计算合理的Y轴范围
   const allData = [...sData, ...dData]
   const validData = allData.filter(v => v > 0)
   const minVal = validData.length ? Math.floor(Math.min(...validData)) - 5 : 60
   const maxVal = validData.length ? Math.ceil(Math.max(...validData)) + 5 : 180
 
-  const series = [
-    {
-      name: '收缩压',
-      data: sData,
-      color: '#ff6a00'
-    },
-    {
-      name: '舒张压',
-      data: dData,
-      color: '#007aff'
-    }
-  ]
+  const series = [ { name: '收缩压', data: sData, color: '#ff6a00' }, { name: '舒张压', data: dData, color: '#007aff' } ]
 
-  // 获取canvas上下文
   let ctx: any = null
   try {
     if (typeof uni !== 'undefined' && typeof uni.createCanvasContext === 'function') {
-      // 小程序环境:优先尝试传入组件实例
-      try {
-        ctx = vm?.proxy ? uni.createCanvasContext('bpChart', vm.proxy) : uni.createCanvasContext('bpChart')
-      } catch (e) {
-        // 再尝试不传vm
-        try { ctx = uni.createCanvasContext('bpChart') } catch (err) { ctx = null }
-      }
+      try { ctx = vm?.proxy ? uni.createCanvasContext('bpChart', vm.proxy) : uni.createCanvasContext('bpChart') } catch (e) { try { ctx = uni.createCanvasContext('bpChart') } catch (err) { ctx = null } }
     }
-  } catch (e) {
-    ctx = null
-  }
+  } catch (e) { ctx = null }
 
-  // H5环境尝试使用DOM获取2D上下文
   if (!ctx && typeof document !== 'undefined') {
     try {
-      // 重试逻辑:初次可能未渲染到 DOM
       let el: HTMLCanvasElement | null = null
       for (let attempt = 0; attempt < 3; attempt++) {
         el = document.getElementById('bpChart') 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)
-        }
+        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; 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) {
-      ctx = null
-    }
+    } catch (e) { 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
-  }
+  if (!ctx) { console.warn('Unable to obtain canvas context for uCharts.'); return }
 
-  console.log('Canvas config:', {
-    width: cssWidth,
-    height: cssHeight,
-    pixelRatio,
-    categoriesLength: categories.length,
-    sDataPoints: sData.length,
-    dDataPoints: dData.length
-  })
-
-  // 简化的uCharts配置 - 关闭所有可能产生重叠的选项
   const config = {
     $this: vm?.proxy,
     canvasId: 'bpChart',
     context: ctx,
     type: 'line',
-    fontSize: 10, // 全局字体大小,参考微信小程序示例
+    fontSize: 10,
     categories: categoriesToUse,
     series: series,
-    // 使用比 canvas 略窄的绘图宽度并设置 padding 来避免右边溢出
     width: chartWidth,
     padding: [10, rightGap + 8, 18, 10],
     height: cssHeight,
-    pixelRatio: pixelRatio,
+    pixelRatio,
     background: 'transparent',
-    animation: false, // 关闭动画避免干扰
+    animation: false,
     enableScroll: false,
-    dataLabel: false, // 关键:关闭数据点标签
-    legend: {
-      show: true,
-      position: 'top',
-      float: 'right',
-      fontSize: 10,
-      spacing: 0
-    },
-    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}mmHg` : '' // 只显示整数值
-    },
-    extra: {
-      line: {
-        type: 'curve',
-        width: 1, // 进一步调细线宽
-        activeType: 'point', // 简化点样式
-        point: {
-          radius: 0.5, // 进一步调小数据点半径
-          strokeWidth: 0.5 // 调小边框宽度
-        }
-      },
-      tooltip: {
-        showBox: false, // 关闭提示框避免重叠
-        showCategory: false
-      }
-    }
+    dataLabel: false,
+    legend: { show: true, position: 'top', float: 'right', fontSize: 10, spacing: 0 },
+    xAxis: { disableGrid: true, axisLine: true, axisLineColor: '#e0e0e0', fontColor: '#666666', fontSize: 10, boundaryGap: 'justify' },
+    yAxis: { disableGrid: false, gridColor: '#f5f5f5', splitNumber: 4, min: minVal, max: maxVal, axisLine: true, axisLineColor: '#e0e0e0', fontColor: '#666666', fontSize: 10, format: (val: number) => val % 1 === 0 ? `${val}mmHg` : '' },
+    extra: { line: { type: 'curve', width: 1, activeType: 'point', point: { radius: 0.5, strokeWidth: 0.5 } }, tooltip: { showBox: false, showCategory: false } }
   }
 
   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 (chartInstance.value && chartInstance.value.destroy) { try { chartInstance.value.destroy() } catch (e) { console.warn('destroy before init failed', e) } chartInstance.value = null; chartInitialized = false }
     chartInstance.value = new uCharts(config)
     chartInitialized = true
-    console.log('uCharts initialized successfully')
-  } catch (error) {
-    console.error('uCharts init error:', error)
-    chartInitialized = false
-  }
+  } catch (error) { console.error('uCharts init error:', error); chartInitialized = false }
   chartBusy = false
 }
 
@@ -467,62 +411,70 @@ async function updateChartData() {
     }
     return
   }
-
   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('')
-    }
-  }
 
-  // 只为有记录的天生成categories和data
-  const sData: number[] = []
-  const dData: number[] = []
+  const categories: string[] = []
+  const dataS: number[] = []
+  const dataD: 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)
+
+  if (viewMode.value === 'month') {
+    const days = daysInMonth(year, month)
+    const showLabelDays: number[] = []
+    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++) categories.push(showLabelDays.includes(d) ? `${d}日` : '')
+
+    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 sortedDays = Array.from(dayMap.keys()).sort((a, b) => a - b)
+    for (const d of sortedDays) { const rec = dayMap.get(d)!; filteredCategories.push(`${d}日`); dataS.push(rec.s); dataD.push(rec.d) }
+  } else {
+    const weekStart = getWeekStart(current.value)
+    const weekDays = ['一','二','三','四','五','六','日']
+    for (let i = 0; i < 7; i++) {
+      const date = new Date(weekStart)
+      date.setDate(weekStart.getDate() + i)
+      categories.push(`${date.getDate()}日(${weekDays[i]})`)
+    }
+
+    for (const r of records.value) {
+      const parts = r.date.split('-')
+      if (parts.length >= 3) {
+        const recordDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10))
+        const weekStartDate = getWeekStart(recordDate)
+        if (weekStartDate.getTime() === weekStart.getTime()) {
+          const dayOfWeek = recordDate.getDay() || 7
+          dayMap.set(dayOfWeek, r)
+        }
+      }
+    }
+    for (let i = 1; i <= 7; i++) {
+      const rec = dayMap.get(i)
+      if (rec) { const date = new Date(rec.date); filteredCategories.push(`${date.getDate()}日(${weekDays[i-1]})`); dataS.push(rec.s); dataD.push(rec.d) }
+    }
   }
-  const sortedDays = Array.from(dayMap.keys()).sort((a, b) => a - b)
-  for (const day of sortedDays) {
-    const rec = dayMap.get(day)!
-    filteredCategories.push(`${day}日`)
-    sData.push(rec.s)
-    dData.push(rec.d)
-  }
-  const categoriesToUse = filteredCategories.length ? filteredCategories : categories
 
-  const allData = [...sData, ...dData]
+  const categoriesToUse = filteredCategories.length ? filteredCategories : categories
+  const allData = [...dataS, ...dataD]
   const validData = allData.filter(v => v > 0)
   const minVal = validData.length ? Math.floor(Math.min(...validData)) - 5 : 60
   const maxVal = validData.length ? Math.ceil(Math.max(...validData)) + 5 : 180
 
   try {
-    // 使用uCharts的更新方法,仅更新有数据的分类和序列
-    // 若实例存在,先更新宽度/padding(如果有配置)以避免右侧溢出
     try {
       const size = await getCanvasSize()
       const cssWidth = size.width
@@ -532,41 +484,17 @@ async function updateChartData() {
         chartInstance.value.opts.width = chartWidth
         chartInstance.value.opts.padding = [10, rightGap + 8, 18, 10]
       }
-    } catch (e) {
-      // 忽略尺寸更新错误
-    }
+    } catch (e) {}
 
-    chartInstance.value.updateData({
-      categories: categoriesToUse,
-      series: [
-        {
-          name: '收缩压',
-          data: sData,
-          color: '#ff6a00'
-        },
-        {
-          name: '舒张压',
-          data: dData,
-          color: '#007aff'
-        }
-      ]
-    })
-
-    // 更新Y轴范围
+    chartInstance.value.updateData({ categories: categoriesToUse, series: [{ name: '收缩压', data: dataS, color: '#ff6a00' }, { name: '舒张压', data: dataD, color: '#007aff' }] })
     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) }
+    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) }
+    try { await drawChart() } catch (e) { console.error('re-init after update failure also failed', e) }
   }
   chartBusy = false
 }
@@ -638,34 +566,49 @@ async function rebuildChart() {
   }
 }
 
-// 其他函数保持不变
-async function prevMonth() {
+// 周/月周期导航与 Picker 处理
+async function prevPeriod() {
   const d = new Date(current.value)
-  d.setMonth(d.getMonth() - 1)
+  if (viewMode.value === 'month') {
+    d.setMonth(d.getMonth() - 1)
+  } else {
+    d.setDate(d.getDate() - 7)
+  }
   current.value = d
-  pickerValue.value = formatPickerDate(d)
+  pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
   records.value = generateMockRecords(d)
   await rebuildChart()
 }
 
-async function nextMonth() {
+async function nextPeriod() {
   const d = new Date(current.value)
-  d.setMonth(d.getMonth() + 1)
+  if (viewMode.value === 'month') {
+    d.setMonth(d.getMonth() + 1)
+  } else {
+    d.setDate(d.getDate() + 7)
+  }
   current.value = d
-  pickerValue.value = formatPickerDate(d)
+  pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
   records.value = generateMockRecords(d)
   await rebuildChart()
 }
 
+async function setViewMode(mode: 'month' | 'week') {
+  if (viewMode.value !== mode) {
+    viewMode.value = mode
+    records.value = generateMockRecords(current.value)
+    await rebuildChart()
+  }
+}
+
 async function onPickerChange(e: any) {
   const val = e?.detail?.value || e
-  const parts = (val as string).split('-')
-  if (parts.length >= 2) {
-    const y = parseInt(parts[0], 10)
-    const m = parseInt(parts[1], 10) - 1
+  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 = formatPickerDate(d)
+    pickerValue.value = [val[0], val[1]]
     records.value = generateMockRecords(d)
     await rebuildChart()
   }
@@ -715,8 +658,17 @@ async function confirmAdd() {
   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()
@@ -766,6 +718,35 @@ async function confirmDeleteRecord(id: string) {
   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