|
@@ -1,5 +1,5 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <CustomNav title="体重" leftType="back" />
|
|
|
|
|
|
|
+ <CustomNav title="体格数据" leftType="back" />
|
|
|
<view class="page">
|
|
<view class="page">
|
|
|
|
|
|
|
|
<view class="header">
|
|
<view class="header">
|
|
@@ -13,6 +13,12 @@
|
|
|
<button :class="['toggle-btn', { active: viewMode === 'month' }]" @click="setViewMode('month')">月</button>
|
|
<button :class="['toggle-btn', { active: viewMode === 'month' }]" @click="setViewMode('month')">月</button>
|
|
|
<button :class="['toggle-btn', { active: viewMode === 'week' }]" @click="setViewMode('week')">周</button>
|
|
<button :class="['toggle-btn', { active: viewMode === 'week' }]" @click="setViewMode('week')">周</button>
|
|
|
</view>
|
|
</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>
|
|
</view>
|
|
|
<button class="btn" @click="nextPeriod">›</button>
|
|
<button class="btn" @click="nextPeriod">›</button>
|
|
|
</view>
|
|
</view>
|
|
@@ -20,23 +26,23 @@
|
|
|
|
|
|
|
|
<!-- 趋势图 - 简化canvas设置 -->
|
|
<!-- 趋势图 - 简化canvas设置 -->
|
|
|
<view class="chart-wrap">
|
|
<view class="chart-wrap">
|
|
|
- <view class="chart-header">本月趋势</view>
|
|
|
|
|
|
|
+ <view class="chart-header">本月趋势</view>
|
|
|
<canvas
|
|
<canvas
|
|
|
- canvas-id="weightChart"
|
|
|
|
|
- id="weightChart"
|
|
|
|
|
|
|
+ canvas-id="bpChart"
|
|
|
|
|
+ id="bpChart"
|
|
|
class="chart-canvas"
|
|
class="chart-canvas"
|
|
|
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
|
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
|
|
></canvas>
|
|
></canvas>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 其他内容保持不变 -->
|
|
|
|
|
<view class="content">
|
|
<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 class="list">
|
|
|
<view v-if="records.length === 0" class="empty">暂无记录,点击右下角 + 添加</view>
|
|
<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="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>
|
|
<button class="btn-delete" @click="confirmDeleteRecord(item.id)">✕</button>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
@@ -48,9 +54,9 @@
|
|
|
|
|
|
|
|
<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">
|
|
|
<view class="drag-handle"></view>
|
|
<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="modal-inner">
|
|
|
<view class="form-row">
|
|
<view class="form-row">
|
|
@@ -60,14 +66,24 @@
|
|
|
</picker>
|
|
</picker>
|
|
|
</view>
|
|
</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">
|
|
<view class="form-row">
|
|
|
<text class="label">体重 (kg)</text>
|
|
<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>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
|
|
+ <!--提供滑动条(收缩压70-200,舒张压40-120)-->
|
|
|
<view class="ruler-wrap">
|
|
<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>
|
|
|
|
|
|
|
|
<view class="fixed-footer">
|
|
<view class="fixed-footer">
|
|
@@ -75,6 +91,7 @@
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
</template>
|
|
</template>
|
|
@@ -85,26 +102,27 @@ import { createUChart } from '@/composables/useUChart'
|
|
|
import CustomNav from '@/components/custom-nav.vue'
|
|
import CustomNav from '@/components/custom-nav.vue'
|
|
|
|
|
|
|
|
import ScaleRuler from '@/components/scale-ruler.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 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'
|
|
// 视图模式:'month' 或 'week'
|
|
|
const viewMode = ref<'month' | 'week'>('month')
|
|
const viewMode = ref<'month' | 'week'>('month')
|
|
|
|
|
|
|
|
-// 年月选择器的选项范围
|
|
|
|
|
|
|
+// 年月选择器的选项范围(与 height/weight 保持一致)
|
|
|
const pickerRange = ref([
|
|
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 初始化以匹配设备宽度)
|
|
// 明确的canvas尺寸(将由 getCanvasSize 初始化以匹配设备宽度)
|
|
|
const canvasWidth = ref(700) // 初始值,会在 mounted 时覆盖
|
|
const canvasWidth = ref(700) // 初始值,会在 mounted 时覆盖
|
|
|
-const canvasHeight = ref(280)
|
|
|
|
|
|
|
+const canvasHeight = ref(320)
|
|
|
|
|
|
|
|
// 获取Canvas实际尺寸的函数 - 参考微信小程序示例使用固定尺寸
|
|
// 获取Canvas实际尺寸的函数 - 参考微信小程序示例使用固定尺寸
|
|
|
function getCanvasSize(): Promise<{ width: number; height: number }> {
|
|
function getCanvasSize(): Promise<{ width: number; height: number }> {
|
|
@@ -112,7 +130,7 @@ function getCanvasSize(): Promise<{ width: number; height: number }> {
|
|
|
// 使用固定尺寸,参考微信小程序示例
|
|
// 使用固定尺寸,参考微信小程序示例
|
|
|
const windowWidth = uni.getSystemInfoSync().windowWidth;
|
|
const windowWidth = uni.getSystemInfoSync().windowWidth;
|
|
|
const width = windowWidth; // 占满屏幕宽度
|
|
const width = windowWidth; // 占满屏幕宽度
|
|
|
- const height = 280 / 750 * windowWidth; // 280rpx转换为px,与CSS高度匹配
|
|
|
|
|
|
|
+ const height = 320 / 750 * windowWidth; // 320rpx转换为px,与CSS高度匹配
|
|
|
resolve({ width, height });
|
|
resolve({ width, height });
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
@@ -122,7 +140,7 @@ function getCanvasSize(): Promise<{ width: number; height: number }> {
|
|
|
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 displayPeriod = computed(() => {
|
|
const displayPeriod = computed(() => {
|
|
|
if (viewMode.value === 'month') {
|
|
if (viewMode.value === 'month') {
|
|
|
return `${displayYear.value}年 ${displayMonth.value}月`
|
|
return `${displayYear.value}年 ${displayMonth.value}月`
|
|
@@ -137,35 +155,31 @@ const records = ref<RecordItem[]>(generateMockRecords(current.value))
|
|
|
|
|
|
|
|
function generateMockRecords(d: Date): RecordItem[] {
|
|
function generateMockRecords(d: Date): RecordItem[] {
|
|
|
const arr: RecordItem[] = []
|
|
const arr: RecordItem[] = []
|
|
|
- let daysCount: number
|
|
|
|
|
-
|
|
|
|
|
if (viewMode.value === 'month') {
|
|
if (viewMode.value === 'month') {
|
|
|
const y = d.getFullYear()
|
|
const y = d.getFullYear()
|
|
|
const m = d.getMonth()
|
|
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 {
|
|
} 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)
|
|
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))
|
|
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
|
|
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(() => {
|
|
const averageWeight = computed(() => {
|
|
|
if (records.value.length === 0) return '--'
|
|
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)
|
|
// 使用共享日期工具 (src/utils/date.ts)
|
|
|
|
|
|
|
|
-// 使用 createUChart composable (替换本地 uCharts 管理)
|
|
|
|
|
|
|
+// 使用可复用的 chart composable,支持多序列
|
|
|
const vm = getCurrentInstance()
|
|
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(() => {
|
|
onMounted(() => {
|
|
|
|
|
+ // 延迟确保DOM渲染完成并设置canvas尺寸
|
|
|
setTimeout(async () => {
|
|
setTimeout(async () => {
|
|
|
await nextTick()
|
|
await nextTick()
|
|
|
try {
|
|
try {
|
|
@@ -218,27 +285,66 @@ onMounted(() => {
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.warn('getCanvasSize failed on mounted', 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)
|
|
}, 500)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 监听并更新图表(轻微去抖)
|
|
|
watch([() => current.value], async () => {
|
|
watch([() => current.value], async () => {
|
|
|
setTimeout(async () => {
|
|
setTimeout(async () => {
|
|
|
- await weightChart.update(records, current, viewMode)
|
|
|
|
|
|
|
+ await bpChart.update(records, current, viewMode)
|
|
|
}, 100)
|
|
}, 100)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
watch([() => records.value], async () => {
|
|
watch([() => records.value], async () => {
|
|
|
setTimeout(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)
|
|
}, 100)
|
|
|
}, { deep: true })
|
|
}, { 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(() => {
|
|
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() {
|
|
async function prevPeriod() {
|
|
|
const d = new Date(current.value)
|
|
const d = new Date(current.value)
|
|
|
if (viewMode.value === 'month') {
|
|
if (viewMode.value === 'month') {
|
|
@@ -249,7 +355,7 @@ async function prevPeriod() {
|
|
|
current.value = d
|
|
current.value = d
|
|
|
pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
|
|
pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
|
|
|
records.value = generateMockRecords(d)
|
|
records.value = generateMockRecords(d)
|
|
|
- await weightChart.rebuild(records, current, viewMode)
|
|
|
|
|
|
|
+ await rebuildChart()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function nextPeriod() {
|
|
async function nextPeriod() {
|
|
@@ -262,14 +368,14 @@ async function nextPeriod() {
|
|
|
current.value = d
|
|
current.value = d
|
|
|
pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
|
|
pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
|
|
|
records.value = generateMockRecords(d)
|
|
records.value = generateMockRecords(d)
|
|
|
- await weightChart.rebuild(records, current, viewMode)
|
|
|
|
|
|
|
+ await rebuildChart()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function setViewMode(mode: 'month' | 'week') {
|
|
async function setViewMode(mode: 'month' | 'week') {
|
|
|
if (viewMode.value !== mode) {
|
|
if (viewMode.value !== mode) {
|
|
|
viewMode.value = mode
|
|
viewMode.value = mode
|
|
|
records.value = generateMockRecords(current.value)
|
|
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
|
|
current.value = d
|
|
|
pickerValue.value = [val[0], val[1]]
|
|
pickerValue.value = [val[0], val[1]]
|
|
|
records.value = generateMockRecords(d)
|
|
records.value = generateMockRecords(d)
|
|
|
- await weightChart.rebuild(records, current, viewMode)
|
|
|
|
|
|
|
+ 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 addWeight = 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() {
|
|
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 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 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()) {
|
|
|
|
|
- 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' })
|
|
uni.showToast && uni.showToast({ title: '已添加', icon: 'success' })
|
|
|
closeAdd()
|
|
closeAdd()
|
|
|
// 新增记录后彻底重建图表,确保像退出再进入一样刷新
|
|
// 新增记录后彻底重建图表,确保像退出再进入一样刷新
|
|
|
try {
|
|
try {
|
|
|
- await weightChart.rebuild(records, current, viewMode)
|
|
|
|
|
|
|
+ await rebuildChart()
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.warn('rebuildChart after add failed', 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({
|
|
uni.showModal({
|
|
|
title: '删除',
|
|
title: '删除',
|
|
|
content: '确认删除该条记录吗?',
|
|
content: '确认删除该条记录吗?',
|
|
|
success: async (res: any) => {
|
|
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)
|
|
|
- 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)
|
|
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>
|
|
</script>
|
|
|
|
|
|
|
@@ -423,7 +554,7 @@ async function confirmDeleteRecord(id: string) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.chart-wrap {
|
|
.chart-wrap {
|
|
|
- height: 340rpx;
|
|
|
|
|
|
|
+ height: 380rpx;
|
|
|
overflow: hidden; /* 隐藏溢出内容 */
|
|
overflow: hidden; /* 隐藏溢出内容 */
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
border-radius: 12rpx;
|
|
border-radius: 12rpx;
|
|
@@ -441,7 +572,7 @@ async function confirmDeleteRecord(id: string) {
|
|
|
|
|
|
|
|
/* 关键修复:确保canvas样式正确,参考微信小程序示例 */
|
|
/* 关键修复:确保canvas样式正确,参考微信小程序示例 */
|
|
|
.chart-canvas {
|
|
.chart-canvas {
|
|
|
- height: 280rpx;
|
|
|
|
|
|
|
+ height: 320rpx;
|
|
|
background-color: #FFFFFF;
|
|
background-color: #FFFFFF;
|
|
|
display: block;
|
|
display: block;
|
|
|
}
|
|
}
|
|
@@ -519,7 +650,6 @@ async function confirmDeleteRecord(id: string) {
|
|
|
line-height: 56rpx
|
|
line-height: 56rpx
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* 模态框样式保持不变 */
|
|
|
|
|
.modal {
|
|
.modal {
|
|
|
position: fixed;
|
|
position: fixed;
|
|
|
left: 0;
|
|
left: 0;
|
|
@@ -620,6 +750,10 @@ async function confirmDeleteRecord(id: string) {
|
|
|
margin: 12rpx 0
|
|
margin: 12rpx 0
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.ruler-row {
|
|
|
|
|
+ margin-bottom: 8rpx
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.fixed-footer {
|
|
.fixed-footer {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
left: 0;
|
|
left: 0;
|