|
@@ -4,13 +4,18 @@
|
|
|
|
|
|
|
|
<view class="header">
|
|
<view class="header">
|
|
|
<view class="month-selector">
|
|
<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>
|
|
</view>
|
|
|
- <picker mode="date" :value="pickerValue" @change="onPickerChange">
|
|
|
|
|
- <view class="picker-display">切换月份</view>
|
|
|
|
|
- </picker>
|
|
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 趋势图 - 简化canvas设置 -->
|
|
<!-- 趋势图 - 简化canvas设置 -->
|
|
@@ -85,7 +90,16 @@ type RecordItem = { id: string; date: string; weight: number }
|
|
|
|
|
|
|
|
// 当前展示年月
|
|
// 当前展示年月
|
|
|
const current = ref(new Date())
|
|
const current = ref(new Date())
|
|
|
-const pickerValue = ref(formatPickerDate(current.value))
|
|
|
|
|
|
|
+const pickerValue = ref([current.value.getFullYear() - 2000, current.value.getMonth()]) // 年从2000年开始,月0-11
|
|
|
|
|
+
|
|
|
|
|
+// 视图模式:'month' 或 'week'
|
|
|
|
|
+const viewMode = ref<'month' | 'week'>('month')
|
|
|
|
|
+
|
|
|
|
|
+// 年月选择器的选项范围
|
|
|
|
|
+const pickerRange = ref([
|
|
|
|
|
+ Array.from({ length: 50 }, (_, i) => `${2000 + i}年`), // 2000-2049年
|
|
|
|
|
+ Array.from({ length: 12 }, (_, i) => `${i + 1}月`) // 1-12月
|
|
|
|
|
+])
|
|
|
|
|
|
|
|
// 明确的canvas尺寸(将由 getCanvasSize 初始化以匹配设备宽度)
|
|
// 明确的canvas尺寸(将由 getCanvasSize 初始化以匹配设备宽度)
|
|
|
const canvasWidth = ref(700) // 初始值,会在 mounted 时覆盖
|
|
const canvasWidth = ref(700) // 初始值,会在 mounted 时覆盖
|
|
@@ -112,20 +126,49 @@ 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 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))
|
|
const records = ref<RecordItem[]>(generateMockRecords(current.value))
|
|
|
|
|
|
|
|
function generateMockRecords(d: Date): RecordItem[] {
|
|
function generateMockRecords(d: Date): RecordItem[] {
|
|
|
- const y = d.getFullYear()
|
|
|
|
|
- const m = d.getMonth()
|
|
|
|
|
const arr: RecordItem[] = []
|
|
const arr: RecordItem[] = []
|
|
|
- const n = Math.floor(Math.random() * 7)
|
|
|
|
|
|
|
+ let daysCount: number
|
|
|
|
|
+
|
|
|
|
|
+ if (viewMode.value === 'month') {
|
|
|
|
|
+ const y = d.getFullYear()
|
|
|
|
|
+ const m = d.getMonth()
|
|
|
|
|
+ daysCount = daysInMonth(y, m)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ daysCount = 7
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const n = Math.floor(Math.random() * Math.min(daysCount, 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 date = new Date(y, m, day)
|
|
|
|
|
- arr.push({
|
|
|
|
|
- id: `${y}${m}${i}${Date.now()}`,
|
|
|
|
|
- date: formatDisplayDate(date),
|
|
|
|
|
- weight: Number((50 + Math.random() * 50).toFixed(1))
|
|
|
|
|
|
|
+ let recordDate: Date
|
|
|
|
|
+ if (viewMode.value === 'month') {
|
|
|
|
|
+ const y = d.getFullYear()
|
|
|
|
|
+ const m = d.getMonth()
|
|
|
|
|
+ const day = Math.max(1, Math.floor(Math.random() * daysCount) + 1)
|
|
|
|
|
+ recordDate = new Date(y, m, day)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const weekStart = getWeekStart(d)
|
|
|
|
|
+ const dayOffset = Math.floor(Math.random() * 7)
|
|
|
|
|
+ recordDate = new Date(weekStart)
|
|
|
|
|
+ recordDate.setDate(weekStart.getDate() + dayOffset)
|
|
|
|
|
+ }
|
|
|
|
|
+ arr.push({
|
|
|
|
|
+ id: `${recordDate.getTime()}${i}${Date.now()}`,
|
|
|
|
|
+ date: formatDisplayDate(recordDate),
|
|
|
|
|
+ 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))
|
|
@@ -164,260 +207,190 @@ function daysInMonth(year: number, month: number) {
|
|
|
return new Date(year, month + 1, 0).getDate()
|
|
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 绘图 - 修复版本
|
|
// Canvas / uCharts 绘图 - 修复版本
|
|
|
const chartInstance = ref<any>(null)
|
|
const chartInstance = ref<any>(null)
|
|
|
const vm = getCurrentInstance()
|
|
const vm = getCurrentInstance()
|
|
|
let chartInitialized = false
|
|
let chartInitialized = false
|
|
|
let chartBusy = false // 绘图锁,防止并发初始化/更新
|
|
let chartBusy = false // 绘图锁,防止并发初始化/更新
|
|
|
|
|
|
|
|
-// 简化的图表绘制函数
|
|
|
|
|
|
|
+// 简化的图表绘制函数(支持月/周视图)
|
|
|
async function drawChart() {
|
|
async function drawChart() {
|
|
|
- // 防止并发调用
|
|
|
|
|
if (chartBusy) return
|
|
if (chartBusy) return
|
|
|
chartBusy = true
|
|
chartBusy = true
|
|
|
|
|
|
|
|
- // 防止重复初始化(已初始化则更新数据)
|
|
|
|
|
if (chartInitialized && chartInstance.value) {
|
|
if (chartInitialized && chartInstance.value) {
|
|
|
- try {
|
|
|
|
|
- await updateChartData()
|
|
|
|
|
- } finally {
|
|
|
|
|
- chartBusy = false
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ try { await updateChartData() } finally { chartBusy = false }
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 清理旧实例
|
|
|
|
|
if (chartInstance.value) {
|
|
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
|
|
chartInstance.value = null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (typeof uCharts === 'undefined') {
|
|
|
|
|
- console.warn('uCharts not available')
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ 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 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))
|
|
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)
|
|
|
|
|
-
|
|
|
|
|
- // 生成合理的categories - 只显示关键日期
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // categories 构造,区分月/周视图
|
|
|
const categories: string[] = []
|
|
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 data: number[] = []
|
|
const data: number[] = []
|
|
|
const filteredCategories: string[] = []
|
|
const filteredCategories: string[] = []
|
|
|
- // 使用Map按日聚合,保留最新记录(records 数组头部为最新)
|
|
|
|
|
const dayMap = new Map<number, RecordItem>()
|
|
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 d of sortedDays) { const rec = dayMap.get(d)!; filteredCategories.push(`${d}日`); data.push(rec.weight) }
|
|
|
|
|
+ } 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]})`); data.push(rec.weight) }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 将有数据的日期按日顺序输出
|
|
|
|
|
- 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.weight)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 如果没有任何数据,则回退为整月空数据(避免 uCharts 抛错)
|
|
|
|
|
const categoriesToUse = filteredCategories.length ? filteredCategories : categories
|
|
const categoriesToUse = filteredCategories.length ? filteredCategories : categories
|
|
|
|
|
|
|
|
- // 计算合理的Y轴范围
|
|
|
|
|
const validData = data.filter(v => v > 0)
|
|
const validData = data.filter(v => v > 0)
|
|
|
const minVal = validData.length ? Math.floor(Math.min(...validData)) - 2 : 40
|
|
const minVal = validData.length ? Math.floor(Math.min(...validData)) - 2 : 40
|
|
|
const maxVal = validData.length ? Math.ceil(Math.max(...validData)) + 2 : 100
|
|
const maxVal = validData.length ? Math.ceil(Math.max(...validData)) + 2 : 100
|
|
|
|
|
|
|
|
- const series = [{
|
|
|
|
|
- name: '体重',
|
|
|
|
|
- data: data,
|
|
|
|
|
- color: '#ff6a00'
|
|
|
|
|
- }]
|
|
|
|
|
|
|
+ const series = [{ name: '体重', data: data, color: '#ff6a00' }]
|
|
|
|
|
|
|
|
- // 获取canvas上下文
|
|
|
|
|
|
|
+ // 获取 canvas 上下文
|
|
|
let ctx: any = null
|
|
let ctx: any = null
|
|
|
try {
|
|
try {
|
|
|
if (typeof uni !== 'undefined' && typeof uni.createCanvasContext === 'function') {
|
|
if (typeof uni !== 'undefined' && typeof uni.createCanvasContext === 'function') {
|
|
|
- // 小程序环境:优先尝试传入组件实例
|
|
|
|
|
- try {
|
|
|
|
|
- ctx = vm?.proxy ? uni.createCanvasContext('weightChart', vm.proxy) : uni.createCanvasContext('weightChart')
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- // 再尝试不传vm
|
|
|
|
|
- try { ctx = uni.createCanvasContext('weightChart') } catch (err) { ctx = null }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ try { ctx = vm?.proxy ? uni.createCanvasContext('weightChart', vm.proxy) : uni.createCanvasContext('weightChart') } catch (e) { try { ctx = uni.createCanvasContext('weightChart') } catch (err) { ctx = null } }
|
|
|
}
|
|
}
|
|
|
- } catch (e) {
|
|
|
|
|
- ctx = null
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ } catch (e) { ctx = null }
|
|
|
|
|
|
|
|
- // H5环境尝试使用DOM获取2D上下文
|
|
|
|
|
if (!ctx && typeof document !== 'undefined') {
|
|
if (!ctx && typeof document !== 'undefined') {
|
|
|
try {
|
|
try {
|
|
|
- // 重试逻辑:初次可能未渲染到 DOM
|
|
|
|
|
let el: HTMLCanvasElement | null = null
|
|
let el: HTMLCanvasElement | null = null
|
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
|
el = document.getElementById('weightChart') as HTMLCanvasElement | null
|
|
el = document.getElementById('weightChart') as HTMLCanvasElement | null
|
|
|
if (el) break
|
|
if (el) break
|
|
|
- // 短延迟后重试(非阻塞)
|
|
|
|
|
await new Promise(r => setTimeout(r, 50))
|
|
await new Promise(r => setTimeout(r, 50))
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
if (el && el.getContext) {
|
|
if (el && el.getContext) {
|
|
|
- // Ensure canvas actual pixel size matches cssWidth * pixelRatio
|
|
|
|
|
try {
|
|
try {
|
|
|
const physicalW = Math.floor(cssWidth * pixelRatio)
|
|
const physicalW = Math.floor(cssWidth * pixelRatio)
|
|
|
const physicalH = Math.floor(cssHeight * 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)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ 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')
|
|
ctx = el.getContext('2d')
|
|
|
}
|
|
}
|
|
|
- } 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
|
|
|
|
|
|
|
+ } catch (e) { ctx = null }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- console.log('Canvas config:', {
|
|
|
|
|
- width: cssWidth,
|
|
|
|
|
- height: cssHeight,
|
|
|
|
|
- pixelRatio,
|
|
|
|
|
- categoriesLength: categories.length,
|
|
|
|
|
- dataPoints: data.length
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ if (!ctx) { console.warn('Unable to obtain canvas context for uCharts.'); return }
|
|
|
|
|
|
|
|
- // 简化的uCharts配置 - 关闭所有可能产生重叠的选项
|
|
|
|
|
const config = {
|
|
const config = {
|
|
|
$this: vm?.proxy,
|
|
$this: vm?.proxy,
|
|
|
canvasId: 'weightChart',
|
|
canvasId: 'weightChart',
|
|
|
context: ctx,
|
|
context: ctx,
|
|
|
type: 'line',
|
|
type: 'line',
|
|
|
- fontSize: 10, // 全局字体大小,参考微信小程序示例
|
|
|
|
|
|
|
+ fontSize: 10,
|
|
|
categories: categoriesToUse,
|
|
categories: categoriesToUse,
|
|
|
series: series,
|
|
series: series,
|
|
|
- // 使用比 canvas 略窄的绘图宽度并设置 padding 来避免右边溢出
|
|
|
|
|
width: chartWidth,
|
|
width: chartWidth,
|
|
|
padding: [10, rightGap + 8, 18, 10],
|
|
padding: [10, rightGap + 8, 18, 10],
|
|
|
height: cssHeight,
|
|
height: cssHeight,
|
|
|
- pixelRatio: pixelRatio,
|
|
|
|
|
|
|
+ pixelRatio,
|
|
|
background: 'transparent',
|
|
background: 'transparent',
|
|
|
- animation: false, // 关闭动画避免干扰
|
|
|
|
|
|
|
+ animation: false,
|
|
|
enableScroll: 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}kg` : '' // 只显示整数值
|
|
|
|
|
- },
|
|
|
|
|
- extra: {
|
|
|
|
|
- line: {
|
|
|
|
|
- type: 'curve',
|
|
|
|
|
- width: 1, // 进一步调细线宽
|
|
|
|
|
- activeType: 'point', // 简化点样式
|
|
|
|
|
- point: {
|
|
|
|
|
- radius: 0.5, // 进一步调小数据点半径
|
|
|
|
|
- strokeWidth: 0.5 // 调小边框宽度
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- tooltip: {
|
|
|
|
|
- showBox: false, // 关闭提示框避免重叠
|
|
|
|
|
- showCategory: false
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ dataLabel: false,
|
|
|
|
|
+ legend: { show: false },
|
|
|
|
|
+ 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}kg` : '' },
|
|
|
|
|
+ extra: { line: { type: 'curve', width: 1, activeType: 'point', point: { radius: 0.5, strokeWidth: 0.5 } }, tooltip: { showBox: false, showCategory: false } }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
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)
|
|
chartInstance.value = new uCharts(config)
|
|
|
chartInitialized = true
|
|
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
|
|
chartBusy = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -434,50 +407,62 @@ async function updateChartData() {
|
|
|
}
|
|
}
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
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 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 categories: string[] = []
|
|
|
const data: number[] = []
|
|
const data: number[] = []
|
|
|
const filteredCategories: string[] = []
|
|
const filteredCategories: string[] = []
|
|
|
const dayMap = new Map<number, RecordItem>()
|
|
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}日`); data.push(rec.weight) }
|
|
|
|
|
+ } 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]})`); data.push(rec.weight) }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- 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.weight)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+
|
|
|
const categoriesToUse = filteredCategories.length ? filteredCategories : categories
|
|
const categoriesToUse = filteredCategories.length ? filteredCategories : categories
|
|
|
|
|
|
|
|
const validData = data.filter(v => v > 0)
|
|
const validData = data.filter(v => v > 0)
|
|
@@ -485,8 +470,6 @@ async function updateChartData() {
|
|
|
const maxVal = validData.length ? Math.ceil(Math.max(...validData)) + 2 : 100
|
|
const maxVal = validData.length ? Math.ceil(Math.max(...validData)) + 2 : 100
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- // 使用uCharts的更新方法,仅更新有数据的分类和序列
|
|
|
|
|
- // 若实例存在,先更新宽度/padding(如果有配置)以避免右侧溢出
|
|
|
|
|
try {
|
|
try {
|
|
|
const size = await getCanvasSize()
|
|
const size = await getCanvasSize()
|
|
|
const cssWidth = size.width
|
|
const cssWidth = size.width
|
|
@@ -496,34 +479,17 @@ async function updateChartData() {
|
|
|
chartInstance.value.opts.width = chartWidth
|
|
chartInstance.value.opts.width = chartWidth
|
|
|
chartInstance.value.opts.padding = [10, rightGap + 8, 18, 10]
|
|
chartInstance.value.opts.padding = [10, rightGap + 8, 18, 10]
|
|
|
}
|
|
}
|
|
|
- } catch (e) {
|
|
|
|
|
- // 忽略尺寸更新错误
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
|
|
|
- chartInstance.value.updateData({
|
|
|
|
|
- categories: categoriesToUse,
|
|
|
|
|
- series: [{
|
|
|
|
|
- name: '体重',
|
|
|
|
|
- data: data,
|
|
|
|
|
- color: '#ff6a00'
|
|
|
|
|
- }]
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- // 更新Y轴范围
|
|
|
|
|
|
|
+ chartInstance.value.updateData({ categories: categoriesToUse, series: [{ name: '体重', data: data, color: '#ff6a00' }] })
|
|
|
chartInstance.value.opts.yAxis.min = minVal
|
|
chartInstance.value.opts.yAxis.min = minVal
|
|
|
chartInstance.value.opts.yAxis.max = maxVal
|
|
chartInstance.value.opts.yAxis.max = maxVal
|
|
|
-
|
|
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('Update chart error:', 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
|
|
chartInstance.value = null
|
|
|
chartInitialized = false
|
|
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
|
|
chartBusy = false
|
|
|
}
|
|
}
|
|
@@ -595,34 +561,49 @@ async function rebuildChart() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 其他函数保持不变
|
|
|
|
|
-async function prevMonth() {
|
|
|
|
|
|
|
+// 周/月切换和周期导航
|
|
|
|
|
+async function prevPeriod() {
|
|
|
const d = new Date(current.value)
|
|
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
|
|
current.value = d
|
|
|
- pickerValue.value = formatPickerDate(d)
|
|
|
|
|
|
|
+ pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
|
|
|
records.value = generateMockRecords(d)
|
|
records.value = generateMockRecords(d)
|
|
|
await rebuildChart()
|
|
await rebuildChart()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function nextMonth() {
|
|
|
|
|
|
|
+async function nextPeriod() {
|
|
|
const d = new Date(current.value)
|
|
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
|
|
current.value = d
|
|
|
- pickerValue.value = formatPickerDate(d)
|
|
|
|
|
|
|
+ pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
|
|
|
records.value = generateMockRecords(d)
|
|
records.value = generateMockRecords(d)
|
|
|
await rebuildChart()
|
|
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) {
|
|
async function onPickerChange(e: any) {
|
|
|
const val = e?.detail?.value || e
|
|
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)
|
|
const d = new Date(y, m, 1)
|
|
|
current.value = d
|
|
current.value = d
|
|
|
- pickerValue.value = formatPickerDate(d)
|
|
|
|
|
|
|
+ pickerValue.value = [val[0], val[1]]
|
|
|
records.value = generateMockRecords(d)
|
|
records.value = generateMockRecords(d)
|
|
|
await rebuildChart()
|
|
await rebuildChart()
|
|
|
}
|
|
}
|
|
@@ -719,6 +700,35 @@ async function confirmDeleteRecord(id: string) {
|
|
|
gap: 12rpx
|
|
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 {
|
|
.month-label {
|
|
|
font-size: 34rpx;
|
|
font-size: 34rpx;
|
|
|
color: #333
|
|
color: #333
|