|
|
@@ -98,6 +98,7 @@
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import { ref, computed, onMounted, watch, nextTick, onBeforeUnmount, getCurrentInstance } from 'vue'
|
|
|
+import { onShow } from '@dcloudio/uni-app'
|
|
|
import { createUChart } from '@/composables/useUChart'
|
|
|
import CustomNav from '@/components/custom-nav.vue'
|
|
|
|
|
|
@@ -150,38 +151,7 @@ const displayPeriod = computed(() => {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-const records = ref<RecordItem[]>(generateMockRecords(current.value))
|
|
|
-
|
|
|
-function generateMockRecords(d: Date): RecordItem[] {
|
|
|
- const arr: RecordItem[] = []
|
|
|
- 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)
|
|
|
- // 随机生成身高(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 {
|
|
|
- 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 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 })
|
|
|
- }
|
|
|
- }
|
|
|
- return arr.sort((a, b) => (a.date < b.date ? 1 : -1))
|
|
|
-}
|
|
|
+const records = ref<RecordItem[]>([])
|
|
|
|
|
|
// 将 records 聚合为每天一个点(取最新记录)
|
|
|
function aggregateDaily(recordsArr: RecordItem[], year: number, month: number) {
|
|
|
@@ -202,6 +172,98 @@ function aggregateDaily(recordsArr: RecordItem[], year: number, month: number) {
|
|
|
return map
|
|
|
}
|
|
|
|
|
|
+async function fetchRecords() {
|
|
|
+ let startTime = ''
|
|
|
+ let endTime = ''
|
|
|
+ if (viewMode.value === 'month') {
|
|
|
+ const y = current.value.getFullYear()
|
|
|
+ const m = current.value.getMonth()
|
|
|
+ startTime = new Date(y, m, 1).toISOString()
|
|
|
+ // 将结束时间设为当日 23:59:59.999,避免 ISO 时间为当天 00:00
|
|
|
+ const endDate = new Date(y, m + 1, 0)
|
|
|
+ endDate.setHours(23, 59, 59, 999)
|
|
|
+ endTime = endDate.toISOString()
|
|
|
+ } else {
|
|
|
+ const weekStart = getWeekStart(current.value)
|
|
|
+ const weekEnd = getWeekEnd(current.value)
|
|
|
+ // 确保周结束时间也是该日的 23:59:59.999
|
|
|
+ startTime = weekStart.toISOString()
|
|
|
+ try {
|
|
|
+ const we = new Date(weekEnd)
|
|
|
+ we.setHours(23, 59, 59, 999)
|
|
|
+ endTime = we.toISOString()
|
|
|
+ } catch (e) {
|
|
|
+ endTime = weekEnd.toISOString()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 在发送请求前显示 loading 提示;使用 finally 确保在任何情况下都隐藏
|
|
|
+ try {
|
|
|
+ if (typeof uni !== 'undefined' && uni.showLoading) uni.showLoading({ title: '加载中...' })
|
|
|
+ } catch (e) {
|
|
|
+ // ignore
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const token = uni.getStorageSync('token')
|
|
|
+ const res = await uni.request({
|
|
|
+ url: 'https://wx.baiyun.work/physical/list',
|
|
|
+ method: 'POST',
|
|
|
+ data: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 100,
|
|
|
+ startTime,
|
|
|
+ endTime
|
|
|
+ },
|
|
|
+ header: {
|
|
|
+ 'content-type': 'application/json',
|
|
|
+ 'Authorization': `Bearer ${token}`
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (res.statusCode === 401) {
|
|
|
+ // Token 无效,清除并跳转登录
|
|
|
+ uni.removeStorageSync('token')
|
|
|
+ uni.removeStorageSync('role')
|
|
|
+ uni.reLaunch({ url: '/pages/public/login/index' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if ((res.data as any) && (res.data as any).code === 200) {
|
|
|
+ const apiRecords = (res.data as any).data?.records || []
|
|
|
+ // 将后端记录映射为前端格式:确保 height/weight 为数字,优先使用后端返回的 bmi,若无则在客户端计算并保留 1 位小数
|
|
|
+ records.value = apiRecords.map((item: any) => {
|
|
|
+ const h = item.height == null ? 0 : Number(item.height)
|
|
|
+ const w = item.weight == null ? 0 : Number(item.weight)
|
|
|
+ let bmiVal: number | null = null
|
|
|
+ if (item.bmi != null && item.bmi !== '') {
|
|
|
+ const parsed = Number(item.bmi)
|
|
|
+ if (!Number.isNaN(parsed)) bmiVal = Math.round(parsed * 10) / 10
|
|
|
+ }
|
|
|
+ if (bmiVal == null && h > 0 && w > 0) {
|
|
|
+ bmiVal = Math.round((w / ((h / 100) * (h / 100))) * 10) / 10
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ id: String(item.id),
|
|
|
+ date: formatDisplayDate(new Date(item.measureTime)),
|
|
|
+ h,
|
|
|
+ w,
|
|
|
+ bmi: bmiVal ?? 0
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ rebuildChart()
|
|
|
+ } else {
|
|
|
+ console.error('Fetch records failed', res.data)
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Fetch records error', e)
|
|
|
+ } finally {
|
|
|
+ try {
|
|
|
+ if (typeof uni !== 'undefined' && uni.hideLoading) uni.hideLoading()
|
|
|
+ } catch (e) {
|
|
|
+ // ignore
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const averageHeight = computed(() => {
|
|
|
if (records.value.length === 0) return '--'
|
|
|
const sum = records.value.reduce((s, r) => s + r.h, 0)
|
|
|
@@ -287,6 +349,7 @@ onMounted(() => {
|
|
|
|
|
|
// 先从路由读取 metric(如果有),然后创建 chart
|
|
|
initMetricFromRoute()
|
|
|
+ await fetchRecords()
|
|
|
try {
|
|
|
if (!bpChart) bpChart = createChartForMetric(selectedMetric.value)
|
|
|
} catch (e) {
|
|
|
@@ -301,6 +364,15 @@ onMounted(() => {
|
|
|
}, 500)
|
|
|
})
|
|
|
|
|
|
+// 如果在微信小程序端且未登录,自动跳转到登录页
|
|
|
+onShow(() => {
|
|
|
+ const token = uni.getStorageSync('token')
|
|
|
+ if (!token) {
|
|
|
+ // 使用 uni.reLaunch 替代 navigateTo,确保页面栈被清空
|
|
|
+ uni.reLaunch({ url: '/pages/public/login/index' })
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
// 监听并更新图表(轻微去抖)
|
|
|
watch([() => current.value], async () => {
|
|
|
setTimeout(async () => {
|
|
|
@@ -352,7 +424,7 @@ async function prevPeriod() {
|
|
|
}
|
|
|
current.value = d
|
|
|
pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
|
|
|
- records.value = generateMockRecords(d)
|
|
|
+ await fetchRecords()
|
|
|
await rebuildChart()
|
|
|
}
|
|
|
|
|
|
@@ -373,14 +445,14 @@ async function nextPeriod() {
|
|
|
}
|
|
|
current.value = d
|
|
|
pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
|
|
|
- records.value = generateMockRecords(d)
|
|
|
+ await fetchRecords()
|
|
|
await rebuildChart()
|
|
|
}
|
|
|
|
|
|
async function setViewMode(mode: 'month' | 'week') {
|
|
|
if (viewMode.value !== mode) {
|
|
|
viewMode.value = mode
|
|
|
- records.value = generateMockRecords(current.value)
|
|
|
+ await fetchRecords()
|
|
|
await rebuildChart()
|
|
|
}
|
|
|
}
|
|
|
@@ -400,7 +472,7 @@ async function onPickerChange(e: any) {
|
|
|
pickerValue.value = [Number(val[0]), Number(val[1])]
|
|
|
}
|
|
|
current.value = d
|
|
|
- records.value = generateMockRecords(d)
|
|
|
+ await fetchRecords()
|
|
|
await rebuildChart()
|
|
|
}
|
|
|
}
|
|
|
@@ -461,43 +533,44 @@ async function confirmAdd() {
|
|
|
confirmText: '知道了'
|
|
|
})
|
|
|
}
|
|
|
- const id = `user-${Date.now()}`
|
|
|
- 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 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]
|
|
|
+ try {
|
|
|
+ const token = uni.getStorageSync('token')
|
|
|
+ const res = await uni.request({
|
|
|
+ url: 'https://wx.baiyun.work/physical/add',
|
|
|
+ method: 'POST',
|
|
|
+ data: {
|
|
|
+ height: addHeight.value,
|
|
|
+ weight: addWeight.value,
|
|
|
+ measureTime: new Date(addDate.value).toISOString()
|
|
|
+ },
|
|
|
+ header: {
|
|
|
+ 'content-type': 'application/json',
|
|
|
+ 'Authorization': `Bearer ${token}`
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (res.statusCode === 401) {
|
|
|
+ // Token 无效,清除并跳转登录
|
|
|
+ uni.removeStorageSync('token')
|
|
|
+ uni.removeStorageSync('role')
|
|
|
+ uni.reLaunch({ url: '/pages/public/login/index' })
|
|
|
+ return
|
|
|
}
|
|
|
- } 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]
|
|
|
+ if ((res.data as any) && (res.data as any).code === 200) {
|
|
|
+ uni.showToast && uni.showToast({ title: '已添加', icon: 'success' })
|
|
|
+ closeAdd()
|
|
|
+ await fetchRecords()
|
|
|
+ // 新增记录后彻底重建图表,确保像退出再进入一样刷新
|
|
|
+ try {
|
|
|
+ await rebuildChart()
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('rebuildChart after add failed', e)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ uni.showToast && uni.showToast({ title: '添加失败', icon: 'none' })
|
|
|
}
|
|
|
- }
|
|
|
- uni.showToast && uni.showToast({ title: '已添加', icon: 'success' })
|
|
|
- closeAdd()
|
|
|
- // 新增记录后彻底重建图表,确保像退出再进入一样刷新
|
|
|
- try {
|
|
|
- await rebuildChart()
|
|
|
} catch (e) {
|
|
|
- console.warn('rebuildChart after add failed', e)
|
|
|
+ console.error('Add record error', e)
|
|
|
+ uni.showToast && uni.showToast({ title: '添加失败', icon: 'none' })
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -508,8 +581,35 @@ async function confirmDeleteRecord(id: string) {
|
|
|
content: '确认删除该条记录吗?',
|
|
|
success: async (res: any) => {
|
|
|
if (res.confirm) {
|
|
|
- records.value = records.value.filter(r => r.id !== id)
|
|
|
- try { await rebuildChart() } catch (e) { console.warn('rebuildChart after delete failed', e) }
|
|
|
+ try {
|
|
|
+ const token = uni.getStorageSync('token')
|
|
|
+ const delRes = await uni.request({
|
|
|
+ url: 'https://wx.baiyun.work/physical/delete',
|
|
|
+ method: 'POST',
|
|
|
+ data: { id },
|
|
|
+ header: {
|
|
|
+ 'content-type': 'application/json',
|
|
|
+ 'Authorization': `Bearer ${token}`
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (delRes.statusCode === 401) {
|
|
|
+ // Token 无效,清除并跳转登录
|
|
|
+ uni.removeStorageSync('token')
|
|
|
+ uni.removeStorageSync('role')
|
|
|
+ uni.reLaunch({ url: '/pages/public/login/index' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if ((delRes.data as any) && (delRes.data as any).code === 200) {
|
|
|
+ records.value = records.value.filter(r => r.id !== id)
|
|
|
+ uni.showToast && uni.showToast({ title: '已删除', icon: 'success' })
|
|
|
+ try { await rebuildChart() } catch (e) { console.warn('rebuildChart after delete failed', e) }
|
|
|
+ } else {
|
|
|
+ uni.showToast && uni.showToast({ title: '删除失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Delete record error', e)
|
|
|
+ uni.showToast && uni.showToast({ title: '删除失败', icon: 'none' })
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
})
|