Browse Source

chore: auto bump version and changelog [skip ci]

iwt 2 months ago
parent
commit
338ba62315

+ 150 - 0
cfc-frontend/pages/health/bodyfat-confirm.vue

@@ -0,0 +1,150 @@
+<template>
+  <view class="page">
+    <view class="nav-bar">
+      <view class="nav-back" @click="goBack"><text class="back-text">‹ 返回</text></view>
+      <text class="nav-title">体脂秤数据确认</text>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <!-- 已识别指标 -->
+      <view class="card">
+        <view class="card-accent"></view>
+        <view class="card-body">
+          <text class="card-title">AI 已识别指标</text>
+          <text class="card-subtitle">请确认并补充成员信息后保存</text>
+
+          <view v-if="metrics.length === 0" class="empty-state">
+            <text>未识别到指标,请重新拍照</text>
+          </view>
+
+          <view v-for="(m, i) in metrics" :key="i" class="metric-row">
+            <text class="metric-name">{{ m.name }}</text>
+            <view class="metric-val-wrap">
+              <input class="metric-val" type="digit" v-model="m.value" placeholder="数值" />
+              <text class="metric-unit">{{ m.unit || '' }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 绑定成员 -->
+      <view class="card">
+        <view class="card-accent"></view>
+        <view class="card-body">
+          <text class="card-title">绑定成员</text>
+          <picker mode="selector" :range="memberList" range-key="nickname" @change="onMemberChange">
+            <view class="picker-row">
+              <text class="picker-text">{{ currentMember ? currentMember.nickname : '选择成员' }}</text>
+              <text class="picker-arrow">›</text>
+            </view>
+          </picker>
+        </view>
+      </view>
+
+      <view class="save-bar">
+        <button class="save-btn" :disabled="saving" @click="save">{{ saving ? '保存中...' : '保存指标' }}</button>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { saveBodyFatMetrics } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      metrics: [],
+      memberList: [],
+      currentMember: null,
+      saving: false
+    }
+  },
+  onLoad: function(options) {
+    if (options.metrics) {
+      try { this.metrics = JSON.parse(decodeURIComponent(options.metrics)) } catch (e) {}
+    }
+    this.loadMembers()
+  },
+  methods: {
+    loadMembers: function() {
+      var self = this
+      var token = uni.getStorageSync('token')
+      uni.request({
+        url: config.API_BASE_URL + '/api/family/member/list',
+        method: 'POST',
+        data: { visibleOnly: true },
+        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+        success: function(res) {
+          if (res.data && res.data.code === 200 && res.data.data) {
+            self.memberList = res.data.data
+          }
+        }
+      })
+    },
+    onMemberChange: function(e) {
+      var idx = parseInt(e.detail.value)
+      if (idx >= 0 && idx < this.memberList.length) {
+        this.currentMember = this.memberList[idx]
+      }
+    },
+    goBack: function() {
+      uni.navigateBack()
+    },
+    save: function() {
+      var self = this
+      if (!self.currentMember) {
+        uni.showToast({ title: '请选择成员', icon: 'none' })
+        return
+      }
+      if (self.metrics.length === 0) {
+        uni.showToast({ title: '无可保存的指标', icon: 'none' })
+        return
+      }
+      self.saving = true
+      saveBodyFatMetrics({
+        memberId: self.currentMember.id,
+        metrics: self.metrics
+      }).then(function(res) {
+        self.saving = false
+        if (res && res.code === 200) {
+          uni.showToast({ title: '保存成功', icon: 'success' })
+          setTimeout(function() { uni.navigateBack() }, 1000)
+        } else {
+          uni.showToast({ title: (res && res.message) || '保存失败', icon: 'none' })
+        }
+      }).catch(function() {
+        self.saving = false
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #F5F7FA; padding-bottom: 140rpx; }
+.nav-bar { display: flex; align-items: center; padding: 24rpx 30rpx; background: #fff; }
+.nav-back { margin-right: 20rpx; }
+.back-text { font-size: 28rpx; color: #333; }
+.nav-title { font-size: 32rpx; font-weight: bold; color: #333; }
+.content { height: calc(100vh - 100rpx); }
+.card { margin: 24rpx 30rpx; background: #fff; border-radius: 16rpx; overflow: hidden; }
+.card-accent { height: 6rpx; background: linear-gradient(90deg, #F97316, #F59E0B); }
+.card-body { padding: 24rpx; }
+.card-title { font-size: 30rpx; font-weight: bold; color: #333; display: block; }
+.card-subtitle { font-size: 24rpx; color: #999; margin-top: 6rpx; display: block; }
+.empty-state { padding: 40rpx 0; text-align: center; color: #999; font-size: 26rpx; }
+.metric-row { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 0; border-bottom: 1rpx solid #F0F0F0; }
+.metric-row:last-child { border-bottom: none; }
+.metric-name { font-size: 28rpx; color: #333; }
+.metric-val-wrap { display: flex; align-items: center; }
+.metric-val { height: 56rpx; background: #F5F7FA; border-radius: 10rpx; padding: 0 16rpx; font-size: 28rpx; width: 160rpx; text-align: right; }
+.metric-unit { font-size: 26rpx; color: #999; margin-left: 8rpx; }
+.picker-row { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 0; }
+.picker-text { font-size: 28rpx; color: #333; }
+.picker-arrow { font-size: 28rpx; color: #999; }
+.save-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; padding: 20rpx 30rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); }
+.save-btn { width: 100%; background: #F97316; color: #fff; border-radius: 44rpx; height: 84rpx; line-height: 84rpx; font-size: 32rpx; }
+.save-btn[disabled] { opacity: 0.6; }
+</style>

+ 75 - 1
cfc-frontend/pages/health/report-upload.vue

@@ -93,6 +93,13 @@
             <text class="guide-desc">手机端情绪打卡截图或记录文件,追踪情绪变化趋势</text>
           </view>
         </view>
+        <view class="guide-item" @click="goBodyFatScale">
+          <view class="guide-icon-box guide-icon-teal"><text class="guide-icon">⚖️</text></view>
+          <view class="guide-info">
+            <text class="guide-name">体脂秤报告</text>
+            <text class="guide-desc">拍摄体脂秤显示屏照片,AI 自动提取体重、BMI、体脂率等指标</text>
+          </view>
+        </view>
       </view>
       <view class="guide-tips">
         <text class="guide-tips-title">温馨提示</text>
@@ -114,11 +121,18 @@ export default {
       uploading: false,
       selectedFile: null,
       photoPath: null,
-      agreedToTerms: false
+      agreedToTerms: false,
+      currentMemberId: null,
+      extractedMetrics: []
     }
   },
   onLoad: function(options) {
     this.memberId = options.memberId ? parseInt(options.memberId) : null
+    if (options.bodyFatMetrics) {
+      try {
+        this.extractedMetrics = JSON.parse(decodeURIComponent(options.bodyFatMetrics))
+      } catch (e) {}
+    }
   },
   methods: {
     // 切换同意声明
@@ -230,6 +244,66 @@ export default {
         var msg = (err && err.message) || '上传失败,请重试'
         uni.showToast({ title: msg, icon: 'none' })
       })
+    },
+    // ===== 体脂秤专用上传 =====
+    goBodyFatScale() {
+      if (!this.agreedToTerms) {
+        uni.showModal({
+          title: '数据使用声明',
+          content: '上传体脂秤照片即代表您同意平台使用本报告数据进行分析。点击「同意继续」即表示您已阅读并同意。',
+          confirmText: '同意继续',
+          cancelText: '暂不',
+          success: (res) => {
+            if (res.confirm) {
+              this.agreedToTerms = true
+              this.startBodyFatUpload()
+            }
+          }
+        })
+        return
+      }
+      this.startBodyFatUpload()
+    },
+    startBodyFatUpload() {
+      var self = this
+      uni.chooseImage({
+        count: 1,
+        sourceType: ['camera', 'album'],
+        success: function(res) {
+          self.photoPath = res.tempFilePaths[0]
+          self.uploadBodyFat(self.photoPath)
+        }
+      })
+    },
+    uploadBodyFat(filePath) {
+      var self = this
+      self.uploading = true
+      uni.showLoading({ title: 'AI 识别中...' })
+      var token = uni.getStorageSync('token')
+      uni.uploadFile({
+        url: config.API_BASE_URL + '/api/health/report/bodyfat-upload',
+        filePath: filePath,
+        name: 'file',
+        header: { 'Authorization': 'Bearer ' + token },
+        success: function(uploadRes) {
+          var data
+          try { data = JSON.parse(uploadRes.data) } catch (e) { data = uploadRes.data }
+          self.uploading = false
+          uni.hideLoading()
+          if (data && data.code === 200) {
+            self.currentMemberId = data.data && data.data.memberId
+            self.extractedMetrics = data.data && data.data.metrics || []
+            uni.navigateTo({ url: '/pages/health/bodyfat-confirm' })
+          } else {
+            uni.showToast({ title: (data && data.message) || '上传失败', icon: 'none' })
+          }
+        },
+        fail: function() {
+          self.uploading = false
+          uni.hideLoading()
+          uni.showToast({ title: '网络错误', icon: 'none' })
+        }
+      })
     }
   }
 }

+ 21 - 0
cfc-frontend/utils/api.js

@@ -2498,3 +2498,24 @@ export const readNotification = (data) => request('/api/notification/read', 'POS
 export const getDailyTaskOverview = () => {
   return request('/api/daily-task/overview', 'POST', {})
 }
+
+// 体脂秤指标保存
+export const saveBodyFatMetrics = (data) => {
+  return new Promise((resolve, reject) => {
+    const token = uni.getStorageSync('token')
+    uni.request({
+      url: BASE_URL + '/api/health/report/bodyfat-save',
+      method: 'POST',
+      data: data,
+      header: { 'Content-Type': 'application/json', 'Authorization': token ? 'Bearer ' + token : '' },
+      success: (res) => {
+        if (res.data && res.data.code === 200) {
+          resolve(res.data)
+        } else {
+          reject(res.data || { code: -1, message: '保存失败' })
+        }
+      },
+      fail: (err) => reject(err)
+    })
+  })
+}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-4e1991c5314c00a0eb2cb086a42594d1486b976c
+61d8c487c5ca9a0c2855fb6b2438aa2de0cfa6c4