Răsfoiți Sursa

feat(health): 防止用户查看或添加未来的健康数据

- 在血糖、血压、心率和体征页面中禁止选择未来日期
- 添加对月份和周视图下未来日期的限制检查
- 优化日期选择器逻辑,自动纠正未来月份为当前月份
- 更新日期工具函数,支持判断是否为未来日期/月份/周
- 添加提示信息,当用户尝试选择未来日期时进行友好提醒
- 修复可能因未来日期导致的数据异常问题
mcbaiyun 1 lună în urmă
părinte
comite
ecc4382adc

+ 46 - 9
src/pages/patient/health/details/blood-glucose.vue

@@ -94,7 +94,7 @@ import CustomNav from '@/components/custom-nav.vue'
 
 import ScaleRuler from '@/components/scale-ruler.vue'
 import { createUChart } from '@/composables/useUChart'
-import { getWeekStart, getWeekEnd, formatDisplayDate, formatPickerDate, daysInMonth } from '@/utils/date'
+import { getWeekStart, getWeekEnd, formatDisplayDate, formatPickerDate, daysInMonth, getTodayStart, isAfterTodayDate, isMonthAfterToday, isWeekAfterToday } from '@/utils/date'
 import { getWindowWidth } from '@/utils/platform'
 
 type RecordItem = { id: string; date: string; value: number; type: string }
@@ -288,6 +288,8 @@ async function rebuildChart() {
   }
 }
 
+// 使用共享日期工具(在 src/utils/date.ts 中定义)
+
 // 其他函数(含周期切换、picker 处理)
 async function prevPeriod() {
   const d = new Date(current.value)
@@ -306,8 +308,16 @@ async function nextPeriod() {
   const d = new Date(current.value)
   if (viewMode.value === 'month') {
     d.setMonth(d.getMonth() + 1)
+    if (isMonthAfterToday(d)) {
+      uni.showToast && uni.showToast({ title: '不能查看未来的日期', icon: 'none' })
+      return
+    }
   } else {
     d.setDate(d.getDate() + 7)
+    if (isWeekAfterToday(d)) {
+      uni.showToast && uni.showToast({ title: '不能查看未来的日期', icon: 'none' })
+      return
+    }
   }
   current.value = d
   pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
@@ -327,13 +337,20 @@ async function onPickerChange(e: any) {
   // multiSelector 会返回 [yearOffset, monthIndex]
   const val = e?.detail?.value || e
   if (Array.isArray(val) && val.length >= 2) {
-    const y = 2000 + Number(val[0])
-    const m = Number(val[1])
-    const d = new Date(y, m, 1)
-    current.value = d
-    pickerValue.value = [Number(val[0]), Number(val[1])]
-    records.value = generateMockRecords(d)
-    await rebuildChart()
+      const y = 2000 + Number(val[0])
+      const m = Number(val[1])
+      let d = new Date(y, m, 1)
+      if (isMonthAfterToday(d)) {
+        const today = getTodayStart()
+        uni.showToast && uni.showToast({ title: '不能选择未来的月份,已切换到当前月份', icon: 'none' })
+        d = new Date(today.getFullYear(), today.getMonth(), 1)
+        pickerValue.value = [today.getFullYear() - 2000, today.getMonth()]
+      } else {
+        pickerValue.value = [Number(val[0]), Number(val[1])]
+      }
+      current.value = d
+      records.value = generateMockRecords(d)
+      await rebuildChart()
   }
 }
 
@@ -354,7 +371,23 @@ function onRulerChange(v: number) { addGlucose.value = Number(v.toFixed ? Number
 function openAdd() { showAdd.value = true; if (!addGlucose.value) addGlucose.value = 6 }
 function closeAdd() { showAdd.value = false; addGlucose.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
+  const parts = (val || '').split('-')
+  const y = parseInt(parts[0] || '', 10)
+  const m = parseInt(parts[1] || '1', 10) - 1
+  const d = parseInt(parts[2] || '1', 10)
+  const sel = new Date(y, m, d)
+  if (isAfterTodayDate(sel)) {
+    const today = getTodayStart()
+    uni.showToast && uni.showToast({ title: '不能选择未来的日期,已切换到今天', icon: 'none' })
+    addDate.value = formatPickerDate(today)
+    addDateLabel.value = formatDisplayDate(today)
+    return
+  }
+  addDate.value = val
+  addDateLabel.value = val.replace(/^(.{10}).*$/, '$1')
+}
 
 async function confirmAdd() {
   if (!addGlucose.value) {
@@ -383,6 +416,10 @@ async function confirmAdd() {
   const addM = parseInt(parts[1], 10) - 1
   const addD = parseInt(parts[2], 10)
   const addDateObj = new Date(addY, addM, addD)
+  if (isAfterTodayDate(addDateObj)) {
+    uni.showToast && uni.showToast({ title: '不能添加未来日期的数据', icon: 'none' })
+    return
+  }
   let isInCurrentPeriod = false
   if (viewMode.value === 'month') {
     isInCurrentPeriod = addY === current.value.getFullYear() && addM === current.value.getMonth()

+ 41 - 4
src/pages/patient/health/details/blood-pressure.vue

@@ -96,7 +96,7 @@ 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, getTodayStart, isAfterTodayDate, isMonthAfterToday, isWeekAfterToday } from '@/utils/date'
 import { getWindowWidth } from '@/utils/platform'
 
 type RecordItem = { id: string; date: string; s: number; d: number }
@@ -265,6 +265,8 @@ async function rebuildChart() {
   try { await bpChart.rebuild(records, current, viewMode) } catch (e) { console.warn('rebuildChart failed', e) }
 }
 
+// 使用共享日期工具(在 src/utils/date.ts 中定义)
+
 // 周/月周期导航与 Picker 处理
 async function prevPeriod() {
   const d = new Date(current.value)
@@ -283,8 +285,16 @@ async function nextPeriod() {
   const d = new Date(current.value)
   if (viewMode.value === 'month') {
     d.setMonth(d.getMonth() + 1)
+    if (isMonthAfterToday(d)) {
+      uni.showToast && uni.showToast({ title: '不能查看未来的日期', icon: 'none' })
+      return
+    }
   } else {
     d.setDate(d.getDate() + 7)
+    if (isWeekAfterToday(d)) {
+      uni.showToast && uni.showToast({ title: '不能查看未来的日期', icon: 'none' })
+      return
+    }
   }
   current.value = d
   pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
@@ -305,9 +315,17 @@ async function onPickerChange(e: any) {
   if (Array.isArray(val) && val.length >= 2) {
     const y = 2000 + val[0]
     const m = val[1]
-    const d = new Date(y, m, 1)
+    let d = new Date(y, m, 1)
+    // 不允许选择未来的月份
+    if (isMonthAfterToday(d)) {
+      const today = getTodayStart()
+      uni.showToast && uni.showToast({ title: '不能选择未来的月份,已切换到当前月份', icon: 'none' })
+      d = new Date(today.getFullYear(), today.getMonth(), 1)
+      pickerValue.value = [today.getFullYear() - 2000, today.getMonth()]
+    } else {
+      pickerValue.value = [val[0], val[1]]
+    }
     current.value = d
-    pickerValue.value = [val[0], val[1]]
     records.value = generateMockRecords(d)
     await rebuildChart()
   }
@@ -338,6 +356,19 @@ function closeAdd() {
 
 function onAddDateChange(e: any) { 
   const val = e?.detail?.value || e; 
+  // 拦截未来日期
+  const parts = (val || '').split('-')
+  const y = parseInt(parts[0] || '', 10)
+  const m = parseInt(parts[1] || '1', 10) - 1
+  const d = parseInt(parts[2] || '1', 10)
+  const sel = new Date(y, m, d)
+  if (isAfterTodayDate(sel)) {
+    const today = getTodayStart()
+    uni.showToast && uni.showToast({ title: '不能选择未来的日期,已切换到今天', icon: 'none' })
+    addDate.value = formatPickerDate(today)
+    addDateLabel.value = formatDisplayDate(today)
+    return
+  }
   addDate.value = val; 
   addDateLabel.value = val.replace(/^(.{10}).*$/, '$1') 
 }
@@ -367,12 +398,18 @@ async function confirmAdd() {
   const parts = addDate.value.split('-')
   const addY = parseInt(parts[0], 10)
   const addM = parseInt(parts[1], 10) - 1
+  const addD = parseInt(parts[2] || '1', 10)
+  const addDateObj = new Date(addY, addM, addD)
+  if (isAfterTodayDate(addDateObj)) {
+    uni.showToast && uni.showToast({ title: '不能添加未来日期的数据', icon: 'none' })
+    return
+  }
   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))
+    // addDateObj already computed above
     const addWeekStart = getWeekStart(addDateObj)
     const curWeekStart = getWeekStart(current.value)
     if (addWeekStart.getTime() === curWeekStart.getTime()) {

+ 41 - 5
src/pages/patient/health/details/heart-rate.vue

@@ -86,7 +86,7 @@ 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, getTodayStart, isAfterTodayDate, isMonthAfterToday, isWeekAfterToday } from '@/utils/date'
 import { getWindowWidth } from '@/utils/platform'
 
 type RecordItem = { id: string; date: string; hr: number }
@@ -240,6 +240,8 @@ async function rebuildChart() {
   try { await hrChart.rebuild(records, current, viewMode) } catch (e) { console.warn('rebuildChart failed', e) }
 }
 
+// 使用共享日期工具(在 src/utils/date.ts 中定义)
+
 // 周/月周期切换与 picker 处理
 async function prevPeriod() {
   const d = new Date(current.value)
@@ -253,8 +255,19 @@ async function prevPeriod() {
 
 async function nextPeriod() {
   const d = new Date(current.value)
-  if (viewMode.value === 'month') d.setMonth(d.getMonth() + 1)
-  else d.setDate(d.getDate() + 7)
+  if (viewMode.value === 'month') {
+    d.setMonth(d.getMonth() + 1)
+    if (isMonthAfterToday(d)) {
+      uni.showToast && uni.showToast({ title: '不能查看未来的日期', icon: 'none' })
+      return
+    }
+  } else {
+    d.setDate(d.getDate() + 7)
+    if (isWeekAfterToday(d)) {
+      uni.showToast && uni.showToast({ title: '不能查看未来的日期', icon: 'none' })
+      return
+    }
+  }
   current.value = d
   pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
   records.value = generateMockRecords(d)
@@ -274,9 +287,16 @@ async function onPickerChange(e: any) {
   if (Array.isArray(val) && val.length >= 2) {
     const y = 2000 + Number(val[0])
     const m = Number(val[1])
-    const d = new Date(y, m, 1)
+    let d = new Date(y, m, 1)
+    if (isMonthAfterToday(d)) {
+      const today = getTodayStart()
+      uni.showToast && uni.showToast({ title: '不能选择未来的月份,已切换到当前月份', icon: 'none' })
+      d = new Date(today.getFullYear(), today.getMonth(), 1)
+      pickerValue.value = [today.getFullYear() - 2000, today.getMonth()]
+    } else {
+      pickerValue.value = [Number(val[0]), Number(val[1])]
+    }
     current.value = d
-    pickerValue.value = [Number(val[0]), Number(val[1])]
     records.value = generateMockRecords(d)
     await rebuildChart()
   }
@@ -308,6 +328,18 @@ function closeAdd() {
 
 function onAddDateChange(e: any) {
   const val = e?.detail?.value || e
+  const parts = (val || '').split('-')
+  const y = parseInt(parts[0] || '', 10)
+  const m = parseInt(parts[1] || '1', 10) - 1
+  const d = parseInt(parts[2] || '1', 10)
+  const sel = new Date(y, m, d)
+  if (isAfterTodayDate(sel)) {
+    const today = getTodayStart()
+    uni.showToast && uni.showToast({ title: '不能选择未来的日期,已切换到今天', icon: 'none' })
+    addDate.value = formatPickerDate(today)
+    addDateLabel.value = formatDisplayDate(today)
+    return
+  }
   addDate.value = val
   addDateLabel.value = val.replace(/^(.{10}).*$/, '$1')
 }
@@ -324,6 +356,10 @@ async function confirmAdd() {
   const addM = parseInt(parts[1], 10) - 1
   const addD = parseInt(parts[2], 10)
   const addDateObj = new Date(addY, addM, addD)
+  if (isAfterTodayDate(addDateObj)) {
+    uni.showToast && uni.showToast({ title: '不能添加未来日期的数据', icon: 'none' })
+    return
+  }
   let isInCurrentPeriod = false
   if (viewMode.value === 'month') {
     isInCurrentPeriod = addY === current.value.getFullYear() && addM === current.value.getMonth()

+ 38 - 6
src/pages/patient/health/details/physical.vue

@@ -102,7 +102,7 @@ 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, getTodayStart, isAfterTodayDate, isMonthAfterToday, isWeekAfterToday } from '@/utils/date'
 import { getWindowWidth, rpxToPx } from '@/utils/platform'
 
 type RecordItem = { id: string; date: string; h: number; w: number; bmi: number }
@@ -342,7 +342,6 @@ onBeforeUnmount(() => {
 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)
@@ -361,8 +360,16 @@ async function nextPeriod() {
   const d = new Date(current.value)
   if (viewMode.value === 'month') {
     d.setMonth(d.getMonth() + 1)
+    if (isMonthAfterToday(d)) {
+      uni.showToast && uni.showToast({ title: '不能查看未来的日期', icon: 'none' })
+      return
+    }
   } else {
     d.setDate(d.getDate() + 7)
+    if (isWeekAfterToday(d)) {
+      uni.showToast && uni.showToast({ title: '不能查看未来的日期', icon: 'none' })
+      return
+    }
   }
   current.value = d
   pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
@@ -381,11 +388,18 @@ async function setViewMode(mode: 'month' | 'week') {
 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)
+    const y = 2000 + Number(val[0])
+    const m = Number(val[1])
+    let d = new Date(y, m, 1)
+    if (isMonthAfterToday(d)) {
+      const today = getTodayStart()
+      uni.showToast && uni.showToast({ title: '不能选择未来的月份,已切换到当前月份', icon: 'none' })
+      d = new Date(today.getFullYear(), today.getMonth(), 1)
+      pickerValue.value = [today.getFullYear() - 2000, today.getMonth()]
+    } else {
+      pickerValue.value = [Number(val[0]), Number(val[1])]
+    }
     current.value = d
-    pickerValue.value = [val[0], val[1]]
     records.value = generateMockRecords(d)
     await rebuildChart()
   }
@@ -416,6 +430,18 @@ function closeAdd() {
 
 function onAddDateChange(e: any) { 
   const val = e?.detail?.value || e; 
+  const parts = (val || '').split('-')
+  const y = parseInt(parts[0] || '', 10)
+  const m = parseInt(parts[1] || '1', 10) - 1
+  const d = parseInt(parts[2] || '1', 10)
+  const sel = new Date(y, m, d)
+  if (isAfterTodayDate(sel)) {
+    const today = getTodayStart()
+    uni.showToast && uni.showToast({ title: '不能选择未来的日期,已切换到今天', icon: 'none' })
+    addDate.value = formatPickerDate(today)
+    addDateLabel.value = formatDisplayDate(today)
+    return
+  }
   addDate.value = val; 
   addDateLabel.value = val.replace(/^(.{10}).*$/, '$1') 
 }
@@ -447,6 +473,12 @@ async function confirmAdd() {
   const parts = addDate.value.split('-')
   const addY = parseInt(parts[0], 10)
   const addM = parseInt(parts[1], 10) - 1
+  const addD = parseInt(parts[2] || '1', 10)
+  const addDateObj = new Date(addY, addM, addD)
+  if (isAfterTodayDate(addDateObj)) {
+    uni.showToast && uni.showToast({ title: '不能添加未来日期的数据', icon: 'none' })
+    return
+  }
   if (viewMode.value === 'month') {
     if (addY === current.value.getFullYear() && addM === current.value.getMonth()) {
       records.value = [item, ...records.value]

+ 26 - 0
src/utils/date.ts

@@ -51,3 +51,29 @@ export function weekDayIndex(recordDate: Date, weekStart: Date): number {
   const w = setToLocalMidnight(weekStart).getTime()
   return Math.round((r - w) / 86400000) + 1
 }
+
+// 返回当天(本地午夜)
+export function getTodayStart(): Date {
+  return setToLocalMidnight(new Date())
+}
+
+// 是否严格晚于今天(不包含今天)
+export function isAfterTodayDate(d: Date): boolean {
+  const dd = setToLocalMidnight(d)
+  return dd.getTime() > getTodayStart().getTime()
+}
+
+// 给定某一天,判断该月份是否晚于当前月份
+export function isMonthAfterToday(d: Date): boolean {
+  const today = getTodayStart()
+  if (d.getFullYear() > today.getFullYear()) return true
+  if (d.getFullYear() === today.getFullYear() && d.getMonth() > today.getMonth()) return true
+  return false
+}
+
+// 给定某一天,判断其所在周(周一起算)是否晚于当前周
+export function isWeekAfterToday(d: Date): boolean {
+  const ws = getWeekStart(d)
+  const tws = getWeekStart(getTodayStart())
+  return ws.getTime() > tws.getTime()
+}