Quellcode durchsuchen

feat: add wellness center and tcm diagnosis

abiao vor 1 Monat
Ursprung
Commit
7e66c9185f

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+.DS_Store
+.idea/
+*.iml
+wx84f6c33a9fa3977d*.jpg

+ 14 - 5
app.json

@@ -22,6 +22,7 @@
     "pages/projects/project-success/project-success",
 	"pages/projects/project-search/project-search",
     "pages/pay/pay",
+    "pages/wellness/wellness",
     "pages/customservice/customservice",
     "pages/web/web",
     "pages/parse/parse",
@@ -93,6 +94,14 @@
         "pages/merchant/logistics/logistics",
         "pages/merchant/accounts/accounts"
       ]
+    },
+    {
+      "root": "packageWellness",
+      "pages": [
+        "pages/tcm/diagnosis/diagnosis",
+        "pages/tcm/report/report",
+        "pages/tcm/user-form/user-form"
+      ]
     }
   ],
   "preloadRule": {
@@ -133,10 +142,10 @@
         "text": "购物车"
       },
       {
-        "iconPath": "images/footer/weixin.png",
-        "selectedIconPath": "images/footer/weixinc.png",
-        "pagePath": "pages/customservice/customservice",
-        "text": "客服"
+        "iconPath": "images/footer/wellness.png",
+        "selectedIconPath": "images/footer/wellnessc.png",
+        "pagePath": "pages/wellness/wellness",
+        "text": "康养"
       },
       {
         "iconPath": "images/footer/my.png",
@@ -153,4 +162,4 @@
 	        "provider": "wx2b03c6e691cd7370"
 	    }
 	}
-}
+}

BIN
images/footer/wellness.png


BIN
images/footer/wellnessc.png


+ 751 - 0
packageWellness/pages/tcm/diagnosis/diagnosis.js

@@ -0,0 +1,751 @@
+var tcm = require('../../../../utils/tcm.js')
+
+var EXAMPLE_IMAGES = {
+  tongue: 'https://cfohow.hiwavo.com/fohow-app/xcx/tcm/sheup.png',
+  tongueBottom: 'https://cfohow.hiwavo.com/fohow-app/xcx/tcm/shedown.png',
+  face: 'https://cfohow.hiwavo.com/fohow-app/xcx/tcm/face.png'
+}
+
+var USER_OPTION_LABELS = {
+  country: {
+    china: '中国',
+    russia: '俄罗斯',
+    kazakhstan: '哈萨克斯坦',
+    mongolia: '蒙古',
+    japan: '日本',
+    korea: '韩国',
+    usa: '美国',
+    uk: '英国',
+    germany: '德国',
+    france: '法国',
+    other: '其他'
+  },
+  race: {
+    asian: '亚裔',
+    caucasian: '白人',
+    african: '非裔',
+    hispanic: '拉丁裔',
+    mixed: '混血',
+    other: '其他'
+  },
+  occupation: {
+    sedentary_office: '久坐办公(文员/行政/财务等)',
+    it_programmer: 'IT/程序员',
+    driver: '司机(出租车/货运/公交等)',
+    teacher: '教师/培训师',
+    medical_staff: '医护人员',
+    service_industry: '服务行业(餐饮/零售/酒店等)',
+    heavy_labor: '重体力劳动(建筑/搬运/制造等)',
+    outdoor_worker: '户外工作者(农业/快递/外卖等)',
+    student: '学生',
+    homemaker: '家庭主妇/夫',
+    retired: '退休人员',
+    other: '其他'
+  }
+}
+
+Page({
+  data: {
+    currentStep: 0,
+    loadingUsers: true,
+    loadingTips: true,
+    loadingQuestions: false,
+    submitting: false,
+    showUserPopup: false,
+    showExamplePopup: false,
+    exampleTitle: '',
+    exampleImage: '',
+    currentUser: null,
+    scaleUsers: [],
+    photoTipsTitle: '拍摄注意事项',
+    photoTips: [
+      '自然光或均匀灯光下拍摄,避免逆光、偏暗与强曝光。',
+      '保持镜头稳定,对焦清晰,确保舌面纹理可见。',
+      '舌下与面部照片请正对镜头,避免大角度侧拍。'
+    ],
+    uploadCards: [
+      {
+        key: 'tongue',
+        title: '舌面',
+        desc: '请伸出舌头,拍摄舌面正面照,需清晰展示舌苔颜色和纹理',
+        required: true,
+        localPath: '',
+        url: '',
+        uploading: false
+      },
+      {
+        key: 'tongueBottom',
+        title: '舌下',
+        desc: '请翘起舌头,拍摄舌底照片,需清晰展示舌下络脉',
+        required: true,
+        localPath: '',
+        url: '',
+        uploading: false
+      },
+      {
+        key: 'face',
+        title: '面部',
+        desc: '请正对镜头拍摄面部照片,需清晰展示面部整体气色',
+        required: true,
+        localPath: '',
+        url: '',
+        uploading: false
+      },
+      {
+        key: 'body',
+        title: '全身',
+        desc: '请拍摄全身正面照片,用于辅助体态与整体状态分析,可选上传',
+        required: false,
+        localPath: '',
+        url: '',
+        uploading: false
+      }
+    ],
+    questionList: [],
+    extraNote: ''
+  },
+
+  onLoad: function () {
+    this.loadBaseData()
+  },
+
+  onShow: function () {
+    if (wx.getStorageSync('tcmUserChanged')) {
+      wx.removeStorageSync('tcmUserChanged')
+      this.loadUsers()
+    }
+  },
+
+  onUnload: function () {
+    wx.removeStorageSync('tcmEditScaleUser')
+  },
+
+  loadBaseData: function () {
+    this.loadUsers()
+    this.loadPhotoTips()
+  },
+
+  loadUsers: function () {
+    var that = this
+    that.setData({
+      loadingUsers: true
+    })
+
+    tcm.getScaleUsers().then(function (list) {
+      var scaleUsers = that.decorateScaleUsers(Array.isArray(list) ? list : [])
+      var currentUser = null
+
+      if (scaleUsers.length > 0) {
+        currentUser = scaleUsers[0]
+        for (var i = 0; i < scaleUsers.length; i++) {
+          if (scaleUsers[i].isDefault === true || scaleUsers[i].isDefault === 1) {
+            currentUser = scaleUsers[i]
+            break
+          }
+          if (!currentUser && scaleUsers[i].active === true) {
+            currentUser = scaleUsers[i]
+          }
+        }
+      }
+
+      that.setData({
+        scaleUsers: scaleUsers,
+        currentUser: currentUser,
+        loadingUsers: false
+      })
+    }).catch(function () {
+      that.setData({
+        scaleUsers: [],
+        currentUser: null,
+        loadingUsers: false
+      })
+      wx.showToast({
+        title: '用户列表加载失败',
+        icon: 'none'
+      })
+    })
+  },
+
+  loadPhotoTips: function () {
+    var that = this
+    tcm.getPhotoTips().then(function (data) {
+      if (!data) {
+        return
+      }
+      that.setData({
+        photoTipsTitle: data.title || '拍摄注意事项',
+        photoTips: data.tips && data.tips.length ? data.tips : that.data.photoTips,
+        loadingTips: false
+      })
+    }).catch(function () {
+      that.setData({
+        loadingTips: false
+      })
+    })
+  },
+
+  decorateScaleUsers: function (list) {
+    var nextList = []
+    for (var i = 0; i < list.length; i++) {
+      nextList.push(this.decorateScaleUser(list[i]))
+    }
+    return nextList
+  },
+
+  decorateScaleUser: function (user) {
+    if (!user) {
+      return user
+    }
+    var detailTags = []
+    if (user.country) {
+      detailTags.push(this.getUserOptionLabel('country', user.country))
+    }
+    if (user.race) {
+      detailTags.push(this.getUserOptionLabel('race', user.race))
+    }
+    if (user.occupation) {
+      detailTags.push(this.getUserOptionLabel('occupation', user.occupation))
+    }
+    user.detailTags = detailTags
+    return user
+  },
+
+  getUserOptionLabel: function (group, value) {
+    if (!value || !USER_OPTION_LABELS[group]) {
+      return ''
+    }
+    return USER_OPTION_LABELS[group][value] || value
+  },
+
+  openUserPopup: function () {
+    this.setData({
+      showUserPopup: true
+    })
+  },
+
+  closeUserPopup: function () {
+    this.setData({
+      showUserPopup: false
+    })
+  },
+
+  selectUser: function (e) {
+    var user = this.decorateScaleUser(e.currentTarget.dataset.user)
+    this.setData({
+      currentUser: user,
+      showUserPopup: false
+    })
+    if (user && user.ID) {
+      tcm.setDefaultScaleUser(user.ID)
+    }
+  },
+
+  goUserForm: function () {
+    this.closeUserPopup()
+    wx.navigateTo({
+      url: '/packageWellness/pages/tcm/user-form/user-form'
+    })
+  },
+
+  editUser: function (e) {
+    var user = e.currentTarget.dataset.user
+    wx.setStorageSync('tcmEditScaleUser', user)
+    this.closeUserPopup()
+    wx.navigateTo({
+      url: '/packageWellness/pages/tcm/user-form/user-form?mode=edit'
+    })
+  },
+
+  deleteUser: function (e) {
+    var that = this
+    var user = e.currentTarget.dataset.user
+    if (!user || !user.ID) {
+      return
+    }
+
+      wx.showModal({
+        title: '删除检测人',
+        content: '确认删除“' + user.nickName + '”吗?删除后该成员的康养检测入口将不可用。',
+      confirmColor: '#e35b4e',
+      success: function (res) {
+        if (!res.confirm) {
+          return
+        }
+        tcm.deleteScaleUser(user.ID).then(function () {
+          wx.showToast({
+            title: '删除成功',
+            icon: 'success'
+          })
+          if (that.data.currentUser && that.data.currentUser.ID === user.ID) {
+            that.setData({
+              currentUser: null
+            })
+          }
+          that.loadUsers()
+          that.closeUserPopup()
+        }).catch(function (err) {
+          wx.showToast({
+            title: err && err.message ? err.message : '删除失败',
+            icon: 'none'
+          })
+        })
+      }
+    })
+  },
+
+  viewRecords: function () {
+    if (!this.data.currentUser || !this.data.currentUser.ID) {
+      wx.showToast({
+        title: '请先选择检测人',
+        icon: 'none'
+      })
+      return
+    }
+    wx.navigateTo({
+      url: '/packageWellness/pages/tcm/report/report?scaleUserId=' + this.data.currentUser.ID
+    })
+  },
+
+  chooseImageSource: function (e) {
+    var kind = e.currentTarget.dataset.kind
+    var that = this
+    wx.showActionSheet({
+      itemList: ['拍照', '从相册选择'],
+      success: function (res) {
+        var sourceType = res.tapIndex === 0 ? ['camera'] : ['album']
+        wx.chooseImage({
+          count: 1,
+          sizeType: ['compressed'],
+          sourceType: sourceType,
+          success: function (chooseRes) {
+            var tempPath = chooseRes.tempFilePaths[0]
+            that.uploadImage(kind, tempPath)
+          }
+        })
+      }
+    })
+  },
+
+  uploadImage: function (kind, tempPath) {
+    var that = this
+    var cards = this.data.uploadCards.slice()
+    var cardIndex = this.findUploadCardIndex(kind)
+
+    if (cardIndex < 0) {
+      return
+    }
+
+    cards[cardIndex].localPath = tempPath
+    cards[cardIndex].uploading = true
+    cards[cardIndex].url = ''
+
+    that.setData({
+      uploadCards: cards
+    })
+
+    tcm.uploadObservationImage(tempPath, kind).then(function (data) {
+      var nextCards = that.data.uploadCards.slice()
+      nextCards[cardIndex].uploading = false
+      nextCards[cardIndex].url = data.url || data.urlOss || ''
+      that.setData({
+        uploadCards: nextCards
+      })
+      wx.showToast({
+        title: '上传成功',
+        icon: 'success'
+      })
+    }).catch(function (err) {
+      var failCards = that.data.uploadCards.slice()
+      failCards[cardIndex].uploading = false
+      failCards[cardIndex].localPath = ''
+      failCards[cardIndex].url = ''
+      that.setData({
+        uploadCards: failCards
+      })
+      wx.showToast({
+        title: err && err.message ? err.message : '上传失败',
+        icon: 'none',
+        duration: 3000
+      })
+    })
+  },
+
+  findUploadCardIndex: function (kind) {
+    var cards = this.data.uploadCards
+    for (var i = 0; i < cards.length; i++) {
+      if (cards[i].key === kind) {
+        return i
+      }
+    }
+    return -1
+  },
+
+  openExample: function (e) {
+    var kind = e.currentTarget.dataset.kind
+    var title = e.currentTarget.dataset.title
+    this.setData({
+      showExamplePopup: true,
+      exampleTitle: title,
+      exampleImage: EXAMPLE_IMAGES[kind] || ''
+    })
+  },
+
+  closeExample: function () {
+    this.setData({
+      showExamplePopup: false
+    })
+  },
+
+  noop: function () {},
+
+  previewCurrentImage: function (e) {
+    var src = e.currentTarget.dataset.src
+    if (!src) {
+      return
+    }
+    wx.previewImage({
+      current: src,
+      urls: [src]
+    })
+  },
+
+  goBackStep: function () {
+    if (this.data.currentStep === 1) {
+      this.setData({
+        currentStep: 0
+      })
+      return
+    }
+    wx.navigateBack()
+  },
+
+  goQuestionsStep: function () {
+    if (!this.data.currentUser || !this.data.currentUser.ID) {
+      wx.showToast({
+        title: '请先选择检测人',
+        icon: 'none'
+      })
+      return
+    }
+
+    var missing = []
+    for (var i = 0; i < this.data.uploadCards.length; i++) {
+      if (this.data.uploadCards[i].required && !this.data.uploadCards[i].url) {
+        missing.push(this.data.uploadCards[i].title)
+      }
+    }
+
+    if (missing.length) {
+      wx.showToast({
+        title: '请先上传' + missing.join('、'),
+        icon: 'none'
+      })
+      return
+    }
+
+    this.setData({
+      currentStep: 1
+    })
+
+    if (!this.data.questionList.length) {
+      this.loadQuestions()
+    }
+  },
+
+  loadQuestions: function () {
+    var that = this
+    that.setData({
+      loadingQuestions: true
+    })
+
+    tcm.getQuestions().then(function (questions) {
+      var list = Array.isArray(questions) ? questions : []
+      var nextList = []
+      for (var i = 0; i < list.length; i++) {
+        var item = list[i]
+        nextList.push({
+          key: item.key || '',
+          title: item.title || '',
+          hint: item.hint || '',
+          required: item.required === true,
+          multiSelect: item.multiSelect === true,
+          exclusivePairs: item.exclusivePairs || [],
+          showCustomInput: false,
+          customText: '',
+          options: (item.options || []).map(function (opt) {
+            return {
+              value: opt.value || '',
+              label: opt.label || '',
+              isCustom: opt.isCustom === true,
+              placeholder: opt.placeholder || '',
+              description: opt.description || '',
+              isExclusive: opt.isExclusive === true,
+              selected: false,
+              disabled: false
+            }
+          })
+        })
+      }
+
+      that.setData({
+        questionList: nextList,
+        loadingQuestions: false
+      })
+    }).catch(function () {
+      that.setData({
+        loadingQuestions: false
+      })
+      wx.showToast({
+        title: '问诊题目加载失败',
+        icon: 'none'
+      })
+    })
+  },
+
+  toggleOption: function (e) {
+    var qIndex = e.currentTarget.dataset.qindex
+    var oIndex = e.currentTarget.dataset.oindex
+    var list = this.data.questionList.slice()
+    var question = JSON.parse(JSON.stringify(list[qIndex]))
+    var option = question.options[oIndex]
+
+    if (option.disabled) {
+      return
+    }
+
+    if (option.isCustom) {
+      question.showCustomInput = !question.showCustomInput
+      option.selected = question.showCustomInput
+      if (!question.showCustomInput) {
+        question.customText = ''
+      }
+    } else if (option.isExclusive) {
+      var shouldSelectExclusive = !option.selected
+      for (var i = 0; i < question.options.length; i++) {
+        if (!question.options[i].isCustom) {
+          question.options[i].selected = false
+        }
+      }
+      option.selected = shouldSelectExclusive
+      question.showCustomInput = false
+      question.customText = ''
+      for (var a = 0; a < question.options.length; a++) {
+        if (question.options[a].isCustom) {
+          question.options[a].selected = false
+        }
+      }
+    } else {
+      for (var b = 0; b < question.options.length; b++) {
+        if (question.options[b].isExclusive) {
+          question.options[b].selected = false
+        }
+      }
+
+      if (!question.multiSelect) {
+        for (var c = 0; c < question.options.length; c++) {
+          if (!question.options[c].isCustom) {
+            question.options[c].selected = false
+          }
+        }
+        option.selected = true
+      } else {
+        option.selected = !option.selected
+      }
+
+      if (option.selected) {
+        for (var d = 0; d < question.options.length; d++) {
+          if (!question.options[d].selected || question.options[d].value === option.value) {
+            continue
+          }
+          if (this.isMutuallyExclusive(question, option.value, question.options[d].value)) {
+            question.options[d].selected = false
+          }
+        }
+      }
+    }
+
+    question = this.refreshQuestionDisabledState(question)
+    list[qIndex] = question
+    this.setData({
+      questionList: list
+    })
+  },
+
+  refreshQuestionDisabledState: function (question) {
+    var selectedValues = []
+    for (var i = 0; i < question.options.length; i++) {
+      question.options[i].disabled = false
+      if (question.options[i].selected && !question.options[i].isCustom) {
+        selectedValues.push(question.options[i].value)
+      }
+    }
+
+    for (var j = 0; j < selectedValues.length; j++) {
+      var selectedValue = selectedValues[j]
+      var selectedOption = null
+      for (var m = 0; m < question.options.length; m++) {
+        if (question.options[m].value === selectedValue) {
+          selectedOption = question.options[m]
+          break
+        }
+      }
+
+      if (selectedOption && selectedOption.isExclusive) {
+        for (var n = 0; n < question.options.length; n++) {
+          if (!question.options[n].isCustom && question.options[n].value !== selectedValue) {
+            question.options[n].disabled = true
+          }
+        }
+      }
+
+      for (var p = 0; p < question.options.length; p++) {
+        if (question.options[p].selected || question.options[p].isCustom) {
+          continue
+        }
+        if (this.isMutuallyExclusive(question, selectedValue, question.options[p].value)) {
+          question.options[p].disabled = true
+        }
+      }
+    }
+
+    return question
+  },
+
+  isMutuallyExclusive: function (question, value1, value2) {
+    var pairs = question.exclusivePairs || []
+    for (var i = 0; i < pairs.length; i++) {
+      if (pairs[i].length < 2) {
+        continue
+      }
+      if ((pairs[i][0] === value1 && pairs[i][1] === value2) ||
+        (pairs[i][0] === value2 && pairs[i][1] === value1)) {
+        return true
+      }
+    }
+    return false
+  },
+
+  updateCustomText: function (e) {
+    var qIndex = e.currentTarget.dataset.qindex
+    var list = this.data.questionList.slice()
+    list[qIndex].customText = e.detail.value
+    this.setData({
+      questionList: list
+    })
+  },
+
+  updateExtraNote: function (e) {
+    this.setData({
+      extraNote: e.detail.value
+    })
+  },
+
+  getQuestionAnswerText: function (question) {
+    var texts = []
+    for (var i = 0; i < question.options.length; i++) {
+      if (question.options[i].selected && !question.options[i].isCustom) {
+        texts.push(question.options[i].label)
+      }
+    }
+    if (question.customText) {
+      texts.push(question.customText)
+    }
+    return texts.join(',')
+  },
+
+  submitExam: function () {
+    var that = this
+    if (that.data.submitting) {
+      return
+    }
+
+    for (var i = 0; i < that.data.questionList.length; i++) {
+      var question = that.data.questionList[i]
+      var answerText = that.getQuestionAnswerText(question)
+      if (question.required && !answerText) {
+        wx.showToast({
+          title: '请完成' + question.title,
+          icon: 'none'
+        })
+        return
+      }
+    }
+
+    wx.showModal({
+      title: 'AI 健康分析知情同意',
+      content: '本功能使用人工智能技术对舌象、面部与问诊信息进行分析,结果仅供健康参考,不构成医疗诊断或治疗建议。',
+      confirmText: '继续提交',
+      confirmColor: '#eab86a',
+      success: function (res) {
+        if (res.confirm) {
+          that.doSubmitExam()
+        }
+      }
+    })
+  },
+
+  doSubmitExam: function () {
+    var that = this
+    var questionAnswers = {}
+    var customAnswers = {}
+    var observationImages = {}
+
+    for (var i = 0; i < that.data.uploadCards.length; i++) {
+      if (that.data.uploadCards[i].url) {
+        observationImages[that.data.uploadCards[i].key] = [that.data.uploadCards[i].url]
+      }
+    }
+
+    for (var j = 0; j < that.data.questionList.length; j++) {
+      var question = that.data.questionList[j]
+      var selectedValues = []
+      for (var k = 0; k < question.options.length; k++) {
+        if (question.options[k].selected && !question.options[k].isCustom) {
+          selectedValues.push(question.options[k].value)
+        }
+      }
+      if (selectedValues.length) {
+        questionAnswers[question.key] = selectedValues
+      }
+      if (question.customText) {
+        customAnswers[question.key] = question.customText
+      }
+    }
+
+    that.setData({
+      submitting: true
+    })
+
+    tcm.submitExam({
+      scaleUserId: that.data.currentUser.ID,
+      language: tcm.getLanguageCode(),
+      questionAnswers: questionAnswers,
+      customAnswers: customAnswers,
+      observationImages: observationImages,
+      extraNote: that.data.extraNote || '',
+      additionalText: ''
+    }).then(function (data) {
+      wx.showToast({
+        title: '提交成功',
+        icon: 'success'
+      })
+      that.setData({
+        submitting: false
+      })
+      setTimeout(function () {
+        wx.redirectTo({
+          url: '/packageWellness/pages/tcm/report/report?examId=' + data.examId
+        })
+      }, 400)
+    }).catch(function (err) {
+      that.setData({
+        submitting: false
+      })
+      wx.showToast({
+        title: err && err.message ? err.message : '提交失败,请重试',
+        icon: 'none',
+        duration: 3000
+      })
+    })
+  }
+})

+ 3 - 0
packageWellness/pages/tcm/diagnosis/diagnosis.json

@@ -0,0 +1,3 @@
+{
+  "navigationBarTitleText": "中医诊疗"
+}

+ 120 - 0
packageWellness/pages/tcm/diagnosis/diagnosis.wxml

@@ -0,0 +1,120 @@
+<view class="diagnosis-page">
+  <view class="topbar">
+    <view class="user-switch" bindtap="openUserPopup">
+      <text class="user-name">{{currentUser ? currentUser.nickName : '请选择检测人'}}</text>
+      <text class="user-meta" wx:if="{{currentUser}}">{{currentUser.sex == 1 ? '女' : '男'}} · {{currentUser.height || '--'}}cm</text>
+    </view>
+    <view class="record-btn" bindtap="viewRecords">诊疗记录</view>
+  </view>
+
+  <view wx:if="{{currentUser}}" class="user-detail-card">
+    <view class="user-detail-title">检测人信息</view>
+    <view class="user-detail-tags" wx:if="{{currentUser.detailTags && currentUser.detailTags.length}}">
+      <view class="user-detail-tag" wx:for="{{currentUser.detailTags}}" wx:key="index" wx:for-item="tag">{{tag}}</view>
+    </view>
+    <view wx:else class="user-detail-empty">当前检测人尚未补充国家、人种、职业信息,建议完善后再检测。</view>
+  </view>
+
+  <view wx:if="{{currentStep == 0}}" class="step-page">
+    <view class="tips-card">
+      <view class="tips-title">{{photoTipsTitle}}</view>
+      <view class="tips-item" wx:for="{{photoTips}}" wx:key="index">{{item}}</view>
+    </view>
+
+    <view class="upload-card" wx:for="{{uploadCards}}" wx:key="key">
+      <view class="upload-content">
+        <view class="upload-info">
+          <view class="upload-title"><text class="required-mark" wx:if="{{item.required}}">*</text>{{item.title}}</view>
+          <view class="upload-desc">{{item.desc}}</view>
+          <view class="example-link" wx:if="{{item.key != 'body'}}" bindtap="openExample" data-kind="{{item.key}}" data-title="{{item.title}}">查看示例</view>
+        </view>
+        <view class="upload-box {{item.url ? 'done' : ''}}" bindtap="chooseImageSource" data-kind="{{item.key}}">
+          <image wx:if="{{item.localPath}}" class="upload-image" src="{{item.localPath}}" mode="aspectFill" data-src="{{item.localPath}}" catchtap="previewCurrentImage"></image>
+          <view wx:else class="upload-empty">
+            <view class="upload-icon">+</view>
+            <view class="upload-text">{{item.uploading ? '上传中' : '点击上传'}}</view>
+          </view>
+          <view wx:if="{{item.uploading}}" class="upload-mask">上传中</view>
+        </view>
+      </view>
+    </view>
+  </view>
+
+  <view wx:if="{{currentStep == 1}}" class="step-page question-page">
+    <view class="step-indicator">
+      <view class="step-item active">
+        <view class="step-dot">1</view>
+        <view class="step-label">照片上传</view>
+      </view>
+      <view class="step-line active"></view>
+      <view class="step-item active">
+        <view class="step-dot">2</view>
+        <view class="step-label">问诊填写</view>
+      </view>
+      <view class="step-line"></view>
+      <view class="step-item">
+        <view class="step-dot pending">3</view>
+        <view class="step-label pending">生成报告</view>
+      </view>
+    </view>
+
+    <view wx:if="{{loadingQuestions}}" class="loading-box">问诊题目加载中...</view>
+    <block wx:else>
+      <view class="question-card" wx:for="{{questionList}}" wx:key="key" wx:for-index="qIndex">
+        <view class="question-head">
+          <view class="question-title"><text class="required-mark" wx:if="{{item.required}}">*</text>{{item.title}}</view>
+          <view class="question-mode">{{item.multiSelect ? '多选' : '单选'}}</view>
+        </view>
+        <view class="question-hint" wx:if="{{item.hint}}">{{item.hint}}</view>
+        <view class="question-option {{opt.selected ? 'selected' : ''}} {{opt.disabled ? 'disabled' : ''}}" wx:for="{{item.options}}" wx:key="value" wx:for-item="opt" wx:for-index="oIndex" data-qindex="{{qIndex}}" data-oindex="{{oIndex}}" bindtap="toggleOption">
+          <view class="option-label">{{opt.label}}</view>
+          <view class="option-desc" wx:if="{{opt.description}}">{{opt.description}}</view>
+        </view>
+        <textarea wx:if="{{item.showCustomInput}}" class="custom-input" placeholder="请补充您的情况" data-qindex="{{qIndex}}" bindinput="updateCustomText" value="{{item.customText}}"></textarea>
+        <view class="selected-text" wx:if="{{item.customText}}">{{item.customText}}</view>
+      </view>
+
+      <view class="question-card">
+        <view class="question-title">补充说明</view>
+        <textarea class="custom-input note-input" placeholder="可补充睡眠、饮食、近期不适等信息(选填)" bindinput="updateExtraNote" value="{{extraNote}}"></textarea>
+      </view>
+    </block>
+  </view>
+
+  <view class="bottom-action">
+    <view wx:if="{{currentStep == 0}}" class="action-btn" bindtap="goQuestionsStep">下一步:问诊填写</view>
+    <view wx:if="{{currentStep == 1}}" class="action-group">
+      <view class="secondary-btn" bindtap="goBackStep">返回上一步</view>
+      <view class="action-btn submit {{submitting ? 'disabled-btn' : ''}}" bindtap="submitExam">{{submitting ? '提交中...' : '提交并生成报告'}}</view>
+    </view>
+  </view>
+
+  <view class="mask" wx:if="{{showUserPopup}}" bindtap="closeUserPopup">
+    <view class="popup user-popup" catchtap="noop">
+      <view class="popup-title">选择检测人</view>
+      <view class="user-item {{currentUser && currentUser.ID == item.ID ? 'current-user' : ''}}" wx:for="{{scaleUsers}}" wx:key="ID" data-user="{{item}}" bindtap="selectUser">
+      <view class="user-item-main">
+          <view class="user-item-name">{{item.nickName}}</view>
+          <view class="user-item-meta">{{item.sex == 1 ? '女' : '男'}} · {{item.height || '--'}}cm</view>
+          <view class="user-item-tags" wx:if="{{item.detailTags && item.detailTags.length}}">
+            <view class="user-item-tag" wx:for="{{item.detailTags}}" wx:key="index" wx:for-item="tag">{{tag}}</view>
+          </view>
+        </view>
+        <view class="user-actions">
+          <view class="user-action-btn" catchtap="editUser" data-user="{{item}}">编辑</view>
+          <view class="user-action-btn danger" catchtap="deleteUser" data-user="{{item}}">删除</view>
+          <view class="user-check">{{currentUser && currentUser.ID == item.ID ? '已选' : '选择'}}</view>
+        </view>
+      </view>
+      <view class="popup-add-btn" bindtap="goUserForm">添加新用户</view>
+    </view>
+  </view>
+
+  <view class="mask" wx:if="{{showExamplePopup}}" bindtap="closeExample">
+    <view class="popup example-popup" catchtap="noop">
+      <view class="popup-title">{{exampleTitle}}示例</view>
+      <image wx:if="{{exampleImage}}" class="example-image" src="{{exampleImage}}" mode="aspectFit"></image>
+      <view class="example-hint">请尽量按照示例角度、光线和构图上传照片。</view>
+    </view>
+  </view>
+</view>

+ 482 - 0
packageWellness/pages/tcm/diagnosis/diagnosis.wxss

@@ -0,0 +1,482 @@
+.diagnosis-page {
+  min-height: 100%;
+  background: #f7f4ed;
+  padding: 24rpx 24rpx 180rpx;
+  box-sizing: border-box;
+}
+
+.topbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 24rpx;
+}
+
+.user-switch,
+.user-detail-card,
+.record-btn,
+.tips-card,
+.upload-card,
+.question-card,
+.popup {
+  background: #fff;
+  border-radius: 22rpx;
+  box-shadow: 0 10rpx 22rpx rgba(0, 0, 0, 0.05);
+}
+
+.user-switch {
+  flex: 1;
+  padding: 20rpx 24rpx;
+  margin-right: 18rpx;
+}
+
+.user-name {
+  display: block;
+  font-size: 30rpx;
+  color: #2b2b2b;
+  font-weight: 600;
+}
+
+.user-meta {
+  display: block;
+  margin-top: 8rpx;
+  font-size: 22rpx;
+  color: #999;
+}
+
+.record-btn {
+  width: 180rpx;
+  height: 88rpx;
+  line-height: 88rpx;
+  text-align: center;
+  font-size: 26rpx;
+  color: #9a6a16;
+  background: #f8efe0;
+}
+
+.user-detail-card {
+  padding: 22rpx 24rpx;
+  margin-bottom: 20rpx;
+}
+
+.user-detail-title {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #2b2b2b;
+}
+
+.user-detail-tags,
+.user-item-tags {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: 14rpx;
+}
+
+.user-detail-tag,
+.user-item-tag {
+  margin-right: 12rpx;
+  margin-bottom: 12rpx;
+  padding: 10rpx 16rpx;
+  border-radius: 999rpx;
+  background: #f8f2e6;
+  color: #9a6a16;
+  font-size: 22rpx;
+  line-height: 1.4;
+}
+
+.user-detail-empty {
+  margin-top: 12rpx;
+  font-size: 24rpx;
+  line-height: 1.7;
+  color: #b7802a;
+}
+
+.tips-card {
+  padding: 28rpx;
+  margin-bottom: 20rpx;
+  border: 1rpx solid #efe0be;
+  background: linear-gradient(180deg, #fcf8ef, #ffffff);
+}
+
+.tips-title,
+.question-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #2b2b2b;
+}
+
+.tips-item {
+  position: relative;
+  padding-left: 22rpx;
+  margin-top: 18rpx;
+  font-size: 24rpx;
+  line-height: 1.7;
+  color: #666;
+}
+
+.tips-item::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 16rpx;
+  width: 10rpx;
+  height: 10rpx;
+  border-radius: 50%;
+  background: #eab86a;
+}
+
+.upload-card,
+.question-card {
+  padding: 26rpx;
+  margin-bottom: 20rpx;
+}
+
+.upload-content {
+  display: flex;
+  justify-content: space-between;
+}
+
+.upload-info {
+  flex: 1;
+  padding-right: 20rpx;
+}
+
+.upload-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #2b2b2b;
+}
+
+.required-mark {
+  color: #e35b4e;
+  margin-right: 6rpx;
+}
+
+.upload-desc,
+.question-hint,
+.option-desc,
+.selected-text,
+.example-hint {
+  margin-top: 12rpx;
+  font-size: 24rpx;
+  line-height: 1.7;
+  color: #777;
+}
+
+.example-link {
+  margin-top: 18rpx;
+  font-size: 24rpx;
+  color: #b7802a;
+}
+
+.upload-box {
+  width: 172rpx;
+  height: 172rpx;
+  border-radius: 18rpx;
+  overflow: hidden;
+  position: relative;
+  background: #f7f7f7;
+  border: 1rpx solid #e4e4e4;
+}
+
+.upload-box.done {
+  border-color: #eab86a;
+}
+
+.upload-empty,
+.upload-mask {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.upload-icon {
+  font-size: 56rpx;
+  color: #c9c9c9;
+}
+
+.upload-text {
+  margin-top: 4rpx;
+  font-size: 22rpx;
+  color: #999;
+}
+
+.upload-image {
+  width: 100%;
+  height: 100%;
+}
+
+.upload-mask {
+  position: absolute;
+  left: 0;
+  top: 0;
+  background: rgba(0, 0, 0, 0.35);
+  color: #fff;
+  font-size: 24rpx;
+}
+
+.step-indicator {
+  display: flex;
+  align-items: center;
+  margin-bottom: 22rpx;
+  padding: 22rpx;
+  background: #fff;
+  border-radius: 22rpx;
+}
+
+.step-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.step-dot {
+  width: 54rpx;
+  height: 54rpx;
+  line-height: 54rpx;
+  border-radius: 50%;
+  background: #eab86a;
+  color: #fff;
+  text-align: center;
+  font-size: 24rpx;
+  font-weight: 600;
+}
+
+.step-dot.pending {
+  background: #dedede;
+  color: #999;
+}
+
+.step-line {
+  flex: 1;
+  height: 4rpx;
+  margin: 0 10rpx 26rpx;
+  background: #dedede;
+}
+
+.step-line.active {
+  background: #eab86a;
+}
+
+.step-label {
+  margin-top: 10rpx;
+  font-size: 22rpx;
+  color: #b7802a;
+}
+
+.step-label.pending {
+  color: #999;
+}
+
+.loading-box {
+  padding: 90rpx 0;
+  text-align: center;
+  font-size: 26rpx;
+  color: #999;
+}
+
+.question-head {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+}
+
+.question-mode {
+  font-size: 22rpx;
+  color: #b7802a;
+}
+
+.question-option {
+  margin-top: 16rpx;
+  padding: 20rpx 22rpx;
+  border-radius: 16rpx;
+  background: #f7f7f7;
+  border: 1rpx solid #ececec;
+}
+
+.question-option.selected {
+  background: linear-gradient(135deg, #f0cf97, #eab86a);
+  border-color: #eab86a;
+}
+
+.question-option.selected .option-label,
+.question-option.selected .option-desc {
+  color: #fff;
+}
+
+.question-option.disabled {
+  opacity: 0.4;
+}
+
+.option-label {
+  font-size: 26rpx;
+  color: #2b2b2b;
+}
+
+.custom-input {
+  width: 100%;
+  min-height: 140rpx;
+  margin-top: 16rpx;
+  padding: 18rpx 20rpx;
+  box-sizing: border-box;
+  background: #faf7f1;
+  border-radius: 16rpx;
+  font-size: 24rpx;
+  color: #2b2b2b;
+}
+
+.note-input {
+  min-height: 180rpx;
+}
+
+.bottom-action {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 18rpx 24rpx 34rpx;
+  background: rgba(247, 244, 237, 0.96);
+}
+
+.action-btn,
+.secondary-btn,
+.popup-add-btn {
+  height: 88rpx;
+  border-radius: 999rpx;
+  text-align: center;
+  line-height: 88rpx;
+  font-size: 28rpx;
+}
+
+.action-btn {
+  background: linear-gradient(135deg, #efc57b, #d4932b);
+  color: #fff;
+}
+
+.action-group {
+  display: flex;
+  justify-content: space-between;
+}
+
+.secondary-btn {
+  width: 210rpx;
+  background: #f2ead9;
+  color: #9a6a16;
+}
+
+.action-btn.submit {
+  width: 470rpx;
+}
+
+.disabled-btn {
+  opacity: 0.6;
+}
+
+.mask {
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.45);
+  z-index: 30;
+}
+
+.popup {
+  position: absolute;
+  left: 24rpx;
+  right: 24rpx;
+  bottom: 30rpx;
+  padding: 28rpx;
+}
+
+.popup-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  text-align: center;
+  color: #2b2b2b;
+}
+
+.user-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 20rpx;
+  padding: 22rpx 24rpx;
+  border-radius: 18rpx;
+  background: #faf7f1;
+}
+
+.user-item-main {
+  flex: 1;
+  min-width: 0;
+}
+
+.user-item-name {
+  font-size: 28rpx;
+  color: #2b2b2b;
+  font-weight: 600;
+}
+
+.user-item-meta {
+  margin-top: 8rpx;
+  font-size: 22rpx;
+  color: #999;
+}
+
+.current-user {
+  border: 1rpx solid #eab86a;
+}
+
+.user-actions {
+  display: flex;
+  align-items: center;
+  margin-left: 20rpx;
+}
+
+.user-action-btn {
+  min-width: 72rpx;
+  height: 52rpx;
+  padding: 0 14rpx;
+  box-sizing: border-box;
+  margin-right: 12rpx;
+  border-radius: 999rpx;
+  background: #f2ead9;
+  color: #9a6a16;
+  text-align: center;
+  line-height: 52rpx;
+  font-size: 22rpx;
+}
+
+.user-action-btn.danger {
+  background: #f8e6e4;
+  color: #d85b50;
+}
+
+.user-check {
+  font-size: 24rpx;
+  color: #b7802a;
+}
+
+.popup-add-btn {
+  margin-top: 24rpx;
+  background: #f8efe0;
+  color: #9a6a16;
+}
+
+.example-popup {
+  top: 50%;
+  bottom: auto;
+  transform: translateY(-50%);
+}
+
+.example-image {
+  width: 100%;
+  height: 720rpx;
+  margin-top: 20rpx;
+  border-radius: 18rpx;
+  background: #f3f3f3;
+}

+ 279 - 0
packageWellness/pages/tcm/report/report.js

@@ -0,0 +1,279 @@
+var tcm = require('../../../../utils/tcm.js')
+
+Page({
+  data: {
+    loading: true,
+    status: 'loading',
+    errorMessage: '',
+    examId: '',
+    scaleUserId: '',
+    exam: null,
+    scaleUser: null,
+    latestScaleResult: null,
+    statList: [],
+    questionList: [],
+    sectionList: [],
+    imageList: [],
+    reportTime: ''
+  },
+
+  timer: null,
+  pollCount: 0,
+
+  onLoad: function (options) {
+    this.setData({
+      examId: options.examId || '',
+      scaleUserId: options.scaleUserId || ''
+    })
+    this.loadReport(false)
+  },
+
+  onUnload: function () {
+    this.clearTimer()
+  },
+
+  loadReport: function (silent) {
+    var that = this
+    if (!silent) {
+      that.setData({
+        loading: true,
+        errorMessage: ''
+      })
+    }
+
+    var requestPromise = that.data.examId
+      ? tcm.getExamDetail(that.data.examId)
+      : tcm.getLatestExam(that.data.scaleUserId)
+
+    requestPromise.then(function (data) {
+      var exam = data && data.exam ? data.exam : null
+      var aiSections = data && data.aiSections ? data.aiSections : {}
+      var hasSections = that.buildSectionList(aiSections).length > 0
+
+      if (!exam) {
+        that.setData({
+          loading: false,
+          status: 'empty',
+          exam: null,
+          sectionList: [],
+          imageList: []
+        })
+        return
+      }
+
+      var status = exam.status || 'pending'
+      if (status === 'completed' && !hasSections) {
+        status = 'generating'
+      }
+
+      that.setData({
+        loading: false,
+        status: status,
+        exam: exam,
+        scaleUser: data.scaleUser || null,
+        latestScaleResult: data.latestScaleResult || null,
+        statList: that.buildStatList(data.scaleUser || null, data.latestScaleResult || null),
+        questionList: that.buildQuestionList(data.questionAnswersLocalizedExpanded || {}),
+        sectionList: that.buildSectionList(aiSections),
+        imageList: that.buildImageList(exam.observationImagesOss || exam.observationImages || {}),
+        errorMessage: aiSections && aiSections.error ? aiSections.error : '',
+        reportTime: that.formatDateTime(exam.CreatedAt || '')
+      })
+
+      if (status === 'pending' || status === 'generating') {
+        if (!that.timer) {
+          that.startPolling()
+        }
+      } else {
+        that.clearTimer()
+      }
+    }).catch(function (err) {
+      that.setData({
+        loading: false,
+        status: 'error',
+        errorMessage: err && err.message ? err.message : '报告加载失败'
+      })
+    })
+  },
+
+  buildQuestionList: function (source) {
+    var list = []
+    if (Array.isArray(source)) {
+      return source
+    }
+    for (var key in source) {
+      list.push({
+        title: key,
+        value: Array.isArray(source[key]) ? source[key].join(',') : source[key]
+      })
+    }
+    return list
+  },
+
+  buildSectionList: function (aiSections) {
+    var mapping = [
+      { key: 'general', title: '一般情况', badge: '壹' },
+      { key: 'inquiry', title: '问诊分析', badge: '贰' },
+      { key: 'observation', title: '望诊分析', badge: '叁' },
+      { key: 'constitution', title: '体质辨识', badge: '肆' },
+      { key: 'advice', title: '调养建议', badge: '伍' },
+      { key: 'summary', title: '报告摘要', badge: '陆' }
+    ]
+    var list = []
+    for (var i = 0; i < mapping.length; i++) {
+      if (aiSections && aiSections[mapping[i].key]) {
+        list.push({
+          key: mapping[i].key,
+          badge: mapping[i].badge,
+          title: mapping[i].title,
+          content: aiSections[mapping[i].key]
+        })
+      }
+    }
+    return list
+  },
+
+  buildStatList: function (scaleUser, latestScaleResult) {
+    var list = []
+    if (scaleUser) {
+      if (scaleUser.sex !== undefined && scaleUser.sex !== null) {
+        list.push({
+          label: '性别',
+          value: Number(scaleUser.sex) === 1 ? '女' : '男'
+        })
+      }
+      if (scaleUser.age) {
+        list.push({
+          label: '年龄',
+          value: scaleUser.age + '岁'
+        })
+      }
+      if (scaleUser.height) {
+        list.push({
+          label: '身高',
+          value: scaleUser.height + 'cm'
+        })
+      }
+    }
+
+    if (latestScaleResult) {
+      var weight = latestScaleResult.weight_kg || latestScaleResult.weight
+      if (weight) {
+        list.push({
+          label: '体重',
+          value: this.formatValue(weight) + 'kg'
+        })
+      }
+      if (latestScaleResult.bodyScore) {
+        list.push({
+          label: '评分',
+          value: this.formatValue(latestScaleResult.bodyScore) + '分'
+        })
+      }
+    }
+
+    return list.slice(0, 5)
+  },
+
+  formatDateTime: function (value) {
+    if (!value) {
+      return ''
+    }
+    return String(value).replace('T', ' ').slice(0, 16)
+  },
+
+  formatValue: function (value) {
+    if (value === undefined || value === null || value === '') {
+      return '--'
+    }
+    var num = Number(value)
+    if (isNaN(num)) {
+      return String(value)
+    }
+    if (Math.floor(num) === num) {
+      return String(num)
+    }
+    return num.toFixed(1).replace(/\.0$/, '')
+  },
+
+  buildImageList: function (images) {
+    var mapping = {
+      tongue: '舌面',
+      tongueBottom: '舌下',
+      face: '面部',
+      body: '全身'
+    }
+    var orderedKeys = ['tongue', 'tongueBottom', 'face', 'body']
+    if (typeof images === 'string' && images) {
+      try {
+        images = JSON.parse(images)
+      } catch (err) {
+        images = {}
+      }
+    }
+    var list = []
+    for (var i = 0; i < orderedKeys.length; i++) {
+      var key = orderedKeys[i]
+      var urls = images ? images[key] : null
+      if (Array.isArray(urls) && urls.length) {
+        list.push({
+          title: mapping[key],
+          url: urls[0]
+        })
+      } else if (typeof urls === 'string' && urls) {
+        list.push({
+          title: mapping[key],
+          url: urls
+        })
+      }
+    }
+    return list
+  },
+
+  startPolling: function () {
+    var that = this
+    that.clearTimer()
+    that.pollCount = 0
+    that.timer = setInterval(function () {
+      that.pollCount++
+      if (that.pollCount > 60) {
+        that.clearTimer()
+        that.setData({
+          status: 'error',
+          errorMessage: '报告生成超时,请稍后重新查看'
+        })
+        return
+      }
+      that.loadReport(true)
+    }, 3000)
+  },
+
+  clearTimer: function () {
+    if (this.timer) {
+      clearInterval(this.timer)
+      this.timer = null
+    }
+  },
+
+  previewReportImage: function (e) {
+    var current = e.currentTarget.dataset.src
+    var urls = this.data.imageList.map(function (item) {
+      return item.url
+    })
+    wx.previewImage({
+      current: current,
+      urls: urls
+    })
+  },
+
+  retryReport: function () {
+    this.clearTimer()
+    this.loadReport(false)
+  },
+
+  goDiagnosis: function () {
+    wx.redirectTo({
+      url: '/packageWellness/pages/tcm/diagnosis/diagnosis'
+    })
+  }
+})

+ 3 - 0
packageWellness/pages/tcm/report/report.json

@@ -0,0 +1,3 @@
+{
+  "navigationBarTitleText": "诊疗报告"
+}

+ 104 - 0
packageWellness/pages/tcm/report/report.wxml

@@ -0,0 +1,104 @@
+<view class="report-page">
+  <view wx:if="{{loading}}" class="state-box">报告加载中...</view>
+
+  <view wx:elif="{{status == 'empty'}}" class="state-card">
+    <view class="state-title">暂无诊疗报告</view>
+    <view class="state-desc">完成一次 AI 舌诊后,这里会展示最新报告。</view>
+    <view class="state-btn" bindtap="goDiagnosis">立即检测</view>
+  </view>
+
+  <view wx:elif="{{status == 'pending' || status == 'generating'}}" class="state-card generating-card">
+    <view class="state-title">AI 正在生成报告</view>
+    <view class="state-desc">系统已接收本次诊疗信息,通常需要几十秒,请耐心等待。</view>
+    <view class="loading-dot"></view>
+  </view>
+
+  <view wx:elif="{{status == 'error' || status == 'failed'}}" class="state-card">
+    <view class="state-title">报告暂时不可用</view>
+    <view class="state-desc">{{errorMessage || '请稍后重试'}}</view>
+    <view class="state-btn" bindtap="retryReport">重新加载</view>
+  </view>
+
+  <view wx:else class="report-content">
+    <view class="hero-card">
+      <view class="hero-top">
+        <view class="hero-main">
+          <view class="hero-badge-wrap">
+            <image class="hero-badge-icon" src="/images/footer/wellnessc.png" mode="aspectFit"></image>
+            <view class="hero-badge">中医诊疗</view>
+          </view>
+          <view class="hero-title">AI 舌诊报告</view>
+          <view class="hero-sub">{{scaleUser ? scaleUser.nickName : '当前用户'}}<text wx:if="{{reportTime}}"> · {{reportTime}}</text></view>
+        </view>
+        <view class="hero-status">{{exam && exam.status == 'completed' ? '已完成' : '生成中'}}</view>
+      </view>
+      <view class="hero-stats" wx:if="{{statList.length}}">
+        <view class="hero-stat" wx:for="{{statList}}" wx:key="label">
+          <view class="hero-stat-label">{{item.label}}</view>
+          <view class="hero-stat-value">{{item.value}}</view>
+        </view>
+      </view>
+    </view>
+
+    <view class="info-card" wx:if="{{latestScaleResult}}">
+      <view class="card-head">
+        <view class="card-accent"></view>
+        <view class="card-title">最近体测数据</view>
+      </view>
+      <view class="data-list">
+        <view class="data-item" wx:if="{{latestScaleResult.weight}}">
+          <text class="data-key">体重</text>
+          <text class="data-value">{{latestScaleResult.weight}}</text>
+        </view>
+        <view class="data-item" wx:if="{{latestScaleResult.bmi}}">
+          <text class="data-key">BMI</text>
+          <text class="data-value">{{latestScaleResult.bmi}}</text>
+        </view>
+        <view class="data-item" wx:if="{{latestScaleResult.bodyfat}}">
+          <text class="data-key">体脂率</text>
+          <text class="data-value">{{latestScaleResult.bodyfat}}</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="info-card" wx:if="{{questionList.length}}">
+      <view class="card-head">
+        <view class="card-accent"></view>
+        <view class="card-title">问诊记录</view>
+      </view>
+      <view class="question-item" wx:for="{{questionList}}" wx:key="title">
+        <view class="question-title">{{item.title}}</view>
+        <view class="question-value">{{item.value}}</view>
+      </view>
+    </view>
+
+    <view class="info-card" wx:if="{{imageList.length}}">
+      <view class="card-head">
+        <view class="card-accent"></view>
+        <view class="card-title">检测图片</view>
+      </view>
+      <view class="image-list">
+        <view class="image-item" wx:for="{{imageList}}" wx:key="title">
+          <image class="report-image" src="{{item.url}}" mode="aspectFill" data-src="{{item.url}}" bindtap="previewReportImage"></image>
+          <view class="image-title">{{item.title}}</view>
+        </view>
+      </view>
+    </view>
+
+    <view class="section-card" wx:for="{{sectionList}}" wx:key="title">
+      <view class="section-head">
+        <view class="section-badge">{{item.badge}}</view>
+        <view class="card-title">{{item.title}}</view>
+      </view>
+      <view class="section-content">{{item.content}}</view>
+    </view>
+
+    <view class="disclaimer-card">
+      <view class="card-head">
+        <view class="card-accent warn"></view>
+        <view class="card-title small">AI 生成内容提示</view>
+      </view>
+      <view class="section-content">本报告由人工智能技术辅助生成,分析结果仅供健康参考,不构成医疗诊断或治疗建议。如有明显不适,请及时咨询专业医疗机构和医生。</view>
+    </view>
+  </view>
+</view>

+ 260 - 0
packageWellness/pages/tcm/report/report.wxss

@@ -0,0 +1,260 @@
+.report-page {
+  min-height: 100%;
+  background: #f7f4ed;
+  padding: 24rpx;
+  box-sizing: border-box;
+}
+
+.state-box,
+.state-card,
+.hero-card,
+.info-card,
+.section-card,
+.disclaimer-card {
+  background: #fff;
+  border-radius: 22rpx;
+  box-shadow: 0 10rpx 22rpx rgba(0, 0, 0, 0.05);
+}
+
+.state-box,
+.state-card {
+  padding: 80rpx 40rpx;
+  text-align: center;
+}
+
+.state-title,
+.hero-title,
+.card-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #2b2b2b;
+}
+
+.state-desc,
+.hero-sub,
+.section-content,
+.question-value {
+  margin-top: 16rpx;
+  font-size: 24rpx;
+  color: #666;
+  line-height: 1.8;
+}
+
+.state-btn {
+  width: 240rpx;
+  height: 82rpx;
+  line-height: 82rpx;
+  margin: 28rpx auto 0;
+  border-radius: 999rpx;
+  color: #fff;
+  font-size: 28rpx;
+  background: linear-gradient(135deg, #efc57b, #d4932b);
+}
+
+.generating-card {
+  background: linear-gradient(180deg, #fffaf1, #ffffff);
+}
+
+.loading-dot {
+  width: 28rpx;
+  height: 28rpx;
+  margin: 28rpx auto 0;
+  border-radius: 50%;
+  background: #eab86a;
+  box-shadow: 0 0 0 20rpx rgba(234, 184, 106, 0.16);
+}
+
+.hero-card,
+.info-card,
+.section-card,
+.disclaimer-card {
+  margin-bottom: 20rpx;
+  padding: 28rpx;
+}
+
+.hero-card {
+  background: linear-gradient(135deg, #efe2bd, #eab86a);
+}
+
+.hero-top {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+}
+
+.hero-main {
+  flex: 1;
+  padding-right: 18rpx;
+}
+
+.hero-badge-wrap {
+  display: flex;
+  align-items: center;
+}
+
+.hero-badge-icon {
+  width: 40rpx;
+  height: 40rpx;
+  margin-right: 10rpx;
+}
+
+.hero-badge {
+  display: inline-block;
+  padding: 8rpx 18rpx;
+  border-radius: 999rpx;
+  background: rgba(255, 255, 255, 0.35);
+  color: #6b5630;
+  font-size: 22rpx;
+}
+
+.hero-title {
+  margin-top: 14rpx;
+  color: #4f3d1c;
+}
+
+.hero-sub {
+  color: #6b5630;
+}
+
+.hero-status {
+  min-width: 110rpx;
+  height: 52rpx;
+  padding: 0 18rpx;
+  box-sizing: border-box;
+  border-radius: 999rpx;
+  background: rgba(255, 255, 255, 0.35);
+  color: #7b5e2b;
+  text-align: center;
+  line-height: 52rpx;
+  font-size: 22rpx;
+}
+
+.hero-stats {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: 22rpx;
+  padding-top: 20rpx;
+  border-top: 1rpx solid rgba(255, 255, 255, 0.28);
+}
+
+.hero-stat {
+  width: 33.33%;
+  margin-bottom: 16rpx;
+}
+
+.hero-stat-label {
+  font-size: 22rpx;
+  color: rgba(79, 61, 28, 0.72);
+}
+
+.hero-stat-value {
+  margin-top: 8rpx;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #4f3d1c;
+}
+
+.disclaimer-card {
+  background: #fff9ec;
+  border: 1rpx solid #f0dfbc;
+}
+
+.card-title.small {
+  font-size: 28rpx;
+}
+
+.data-list,
+.image-list {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: 18rpx;
+}
+
+.card-head,
+.section-head {
+  display: flex;
+  align-items: center;
+}
+
+.card-accent {
+  width: 8rpx;
+  height: 28rpx;
+  margin-right: 12rpx;
+  border-radius: 999rpx;
+  background: #d4932b;
+}
+
+.card-accent.warn {
+  background: #e0a643;
+}
+
+.section-badge {
+  width: 46rpx;
+  height: 46rpx;
+  margin-right: 12rpx;
+  border-radius: 50%;
+  background: #f7ead0;
+  color: #b7802a;
+  text-align: center;
+  line-height: 46rpx;
+  font-size: 22rpx;
+  font-weight: 600;
+}
+
+.data-item {
+  width: 33.33%;
+  margin-bottom: 18rpx;
+}
+
+.data-key,
+.question-title,
+.image-title {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.data-value {
+  display: block;
+  margin-top: 10rpx;
+  font-size: 30rpx;
+  color: #b7802a;
+  font-weight: 600;
+}
+
+.question-item {
+  margin-top: 18rpx;
+  padding-top: 18rpx;
+  border-top: 1rpx solid #f1f1f1;
+}
+
+.question-item:first-child {
+  margin-top: 0;
+  padding-top: 0;
+  border-top: 0;
+}
+
+.image-item {
+  width: 31%;
+  margin-right: 3.5%;
+  margin-bottom: 20rpx;
+}
+
+.image-item:nth-child(3n) {
+  margin-right: 0;
+}
+
+.report-image {
+  width: 100%;
+  height: 190rpx;
+  border-radius: 16rpx;
+  background: #f3f3f3;
+}
+
+.image-title {
+  margin-top: 10rpx;
+  text-align: center;
+}
+
+.section-content {
+  white-space: pre-wrap;
+}

+ 353 - 0
packageWellness/pages/tcm/user-form/user-form.js

@@ -0,0 +1,353 @@
+var tcm = require('../../../../utils/tcm.js')
+
+var COUNTRY_OPTIONS = [
+  { value: '', label: '请选择' },
+  { value: 'china', label: '中国' },
+  { value: 'russia', label: '俄罗斯' },
+  { value: 'kazakhstan', label: '哈萨克斯坦' },
+  { value: 'mongolia', label: '蒙古' },
+  { value: 'japan', label: '日本' },
+  { value: 'korea', label: '韩国' },
+  { value: 'usa', label: '美国' },
+  { value: 'uk', label: '英国' },
+  { value: 'germany', label: '德国' },
+  { value: 'france', label: '法国' },
+  { value: 'other', label: '其他' }
+]
+
+var RACE_OPTIONS = [
+  { value: '', label: '请选择' },
+  { value: 'asian', label: '亚裔' },
+  { value: 'caucasian', label: '白人' },
+  { value: 'african', label: '非裔' },
+  { value: 'hispanic', label: '拉丁裔' },
+  { value: 'mixed', label: '混血' },
+  { value: 'other', label: '其他' }
+]
+
+var OCCUPATION_OPTIONS = [
+  { value: '', label: '请选择' },
+  { value: 'sedentary_office', label: '久坐办公(文员/行政/财务等)' },
+  { value: 'it_programmer', label: 'IT/程序员' },
+  { value: 'driver', label: '司机(出租车/货运/公交等)' },
+  { value: 'teacher', label: '教师/培训师' },
+  { value: 'medical_staff', label: '医护人员' },
+  { value: 'service_industry', label: '服务行业(餐饮/零售/酒店等)' },
+  { value: 'heavy_labor', label: '重体力劳动(建筑/搬运/制造等)' },
+  { value: 'outdoor_worker', label: '户外工作者(农业/快递/外卖等)' },
+  { value: 'student', label: '学生' },
+  { value: 'homemaker', label: '家庭主妇/夫' },
+  { value: 'retired', label: '退休人员' },
+  { value: 'other', label: '其他' }
+]
+
+function buildHeightOptions() {
+  var list = []
+  for (var i = 100; i <= 220; i++) {
+    list.push({
+      value: String(i),
+      label: i + ' cm'
+    })
+  }
+  return list
+}
+
+function buildWeightOptions() {
+  var list = []
+  for (var i = 60; i <= 400; i++) {
+    var num = i / 2
+    list.push({
+      value: num.toFixed(1),
+      label: num.toFixed(1) + ' kg'
+    })
+  }
+  return list
+}
+
+var HEIGHT_OPTIONS = buildHeightOptions()
+var TARGET_WEIGHT_OPTIONS = buildWeightOptions()
+
+function findIndexByValue(list, value) {
+  for (var i = 0; i < list.length; i++) {
+    if (String(list[i].value) === String(value)) {
+      return i
+    }
+  }
+  return 0
+}
+
+function findLabelByValue(list, value) {
+  var index = findIndexByValue(list, value)
+  return list[index] ? list[index].label : '请选择'
+}
+
+function formatBirthday(value) {
+  if (!value) {
+    return '1999-01-01'
+  }
+  return String(value).split('T')[0]
+}
+
+function calcAge(birthday) {
+  if (!birthday) {
+    return 25
+  }
+  var date = new Date(birthday)
+  if (isNaN(date.getTime())) {
+    return 25
+  }
+  var now = new Date()
+  var age = now.getFullYear() - date.getFullYear()
+  var monthDiff = now.getMonth() - date.getMonth()
+  var dayDiff = now.getDate() - date.getDate()
+  if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
+    age--
+  }
+  if (age < 1) {
+    age = 1
+  }
+  return age
+}
+
+Page({
+  data: {
+    isEditMode: false,
+    saving: false,
+    sexIndex: 0,
+    sexOptions: ['男', '女'],
+    countryOptions: COUNTRY_OPTIONS,
+    raceOptions: RACE_OPTIONS,
+    occupationOptions: OCCUPATION_OPTIONS,
+    heightOptions: HEIGHT_OPTIONS,
+    targetWeightOptions: TARGET_WEIGHT_OPTIONS,
+    countryIndex: 0,
+    raceIndex: 0,
+    occupationIndex: 0,
+    heightIndex: findIndexByValue(HEIGHT_OPTIONS, '170'),
+    targetWeightIndex: findIndexByValue(TARGET_WEIGHT_OPTIONS, '65.0'),
+    form: {
+      ID: null,
+      active: true,
+      avatar: '',
+      birthday: '1999-01-01',
+      deviceId: 0,
+      height: '170',
+      nickName: '',
+      sex: 0,
+      targetWeight: '65.0',
+      wxUserId: 0,
+      country: '',
+      race: '',
+      occupation: '',
+      createdAt: '',
+      updatedAt: ''
+    }
+  },
+
+  onLoad: function (options) {
+    if (options && options.mode === 'edit') {
+      var user = wx.getStorageSync('tcmEditScaleUser')
+      if (user && user.ID) {
+        this.applyUserData(user)
+      }
+    }
+  },
+
+  applyUserData: function (user) {
+    var birthday = formatBirthday(user.birthday || user.Birthday)
+    var height = String(user.height || 170)
+    var targetWeight = String(user.targetWeight || '65.0')
+    var country = user.country || ''
+    var race = user.race || ''
+    var occupation = user.occupation || ''
+    var sex = Number(user.sex || 0)
+
+    this.setData({
+      isEditMode: true,
+      sexIndex: sex,
+      countryIndex: findIndexByValue(COUNTRY_OPTIONS, country),
+      raceIndex: findIndexByValue(RACE_OPTIONS, race),
+      occupationIndex: findIndexByValue(OCCUPATION_OPTIONS, occupation),
+      heightIndex: findIndexByValue(HEIGHT_OPTIONS, height),
+      targetWeightIndex: findIndexByValue(TARGET_WEIGHT_OPTIONS, targetWeight),
+      form: {
+        ID: user.ID,
+        active: user.active !== false,
+        avatar: user.avatar || '',
+        birthday: birthday,
+        deviceId: user.deviceId || 0,
+        height: height,
+        nickName: user.nickName || '',
+        sex: sex,
+        targetWeight: targetWeight,
+        wxUserId: user.wxUserId || 0,
+        country: country,
+        race: race,
+        occupation: occupation,
+        createdAt: user.createdAt || user.CreatedAt || '',
+        updatedAt: user.updatedAt || user.UpdatedAt || ''
+      }
+    })
+  },
+
+  updateNickName: function (e) {
+    this.setData({
+      'form.nickName': e.detail.value
+    })
+  },
+
+  updateBirthday: function (e) {
+    this.setData({
+      'form.birthday': e.detail.value
+    })
+  },
+
+  changeSex: function (e) {
+    var index = Number(e.detail.value)
+    this.setData({
+      sexIndex: index,
+      'form.sex': index
+    })
+  },
+
+  changeHeight: function (e) {
+    var index = Number(e.detail.value)
+    this.setData({
+      heightIndex: index,
+      'form.height': this.data.heightOptions[index].value
+    })
+  },
+
+  changeTargetWeight: function (e) {
+    var index = Number(e.detail.value)
+    this.setData({
+      targetWeightIndex: index,
+      'form.targetWeight': this.data.targetWeightOptions[index].value
+    })
+  },
+
+  changeCountry: function (e) {
+    var index = Number(e.detail.value)
+    this.setData({
+      countryIndex: index,
+      'form.country': this.data.countryOptions[index].value
+    })
+  },
+
+  changeRace: function (e) {
+    var index = Number(e.detail.value)
+    this.setData({
+      raceIndex: index,
+      'form.race': this.data.raceOptions[index].value
+    })
+  },
+
+  changeOccupation: function (e) {
+    var index = Number(e.detail.value)
+    this.setData({
+      occupationIndex: index,
+      'form.occupation': this.data.occupationOptions[index].value
+    })
+  },
+
+  submitForm: function () {
+    var that = this
+    var form = that.data.form
+    var height = Number(form.height)
+    var targetWeight = Number(form.targetWeight)
+
+    if (!form.nickName) {
+      wx.showToast({
+        title: '请输入昵称',
+        icon: 'none'
+      })
+      return
+    }
+
+    if (form.nickName.length > 20) {
+      wx.showToast({
+        title: '昵称不能超过20个字符',
+        icon: 'none'
+      })
+      return
+    }
+
+    if (!height || height < 100 || height > 220) {
+      wx.showToast({
+        title: '请选择正确身高',
+        icon: 'none'
+      })
+      return
+    }
+
+    if (targetWeight && (targetWeight < 30 || targetWeight > 200)) {
+      wx.showToast({
+        title: '请选择正确目标体重',
+        icon: 'none'
+      })
+      return
+    }
+
+    if (that.data.saving) {
+      return
+    }
+
+    var now = new Date().toISOString()
+    var birthday = form.birthday
+    var payload = {
+      ID: that.data.isEditMode ? form.ID : null,
+      active: true,
+      age: String(calcAge(birthday)),
+      avatar: form.avatar || '',
+      birthday: birthday,
+      createdAt: form.createdAt || now,
+      deviceId: form.deviceId || 0,
+      height: String(height),
+      nickName: form.nickName,
+      sex: Number(form.sex),
+      targetWeight: form.targetWeight || '65.0',
+      updatedAt: now,
+      wxUserId: form.wxUserId || 0,
+      country: form.country || '',
+      race: form.race || '',
+      occupation: form.occupation || ''
+    }
+
+    that.setData({
+      saving: true
+    })
+
+    var submitPromise = that.data.isEditMode ? tcm.updateScaleUser(payload) : tcm.createScaleUser(payload)
+
+    submitPromise.then(function () {
+      wx.setStorageSync('tcmUserChanged', true)
+      wx.showToast({
+        title: that.data.isEditMode ? '更新成功' : '创建成功',
+        icon: 'success'
+      })
+      setTimeout(function () {
+        wx.navigateBack()
+      }, 300)
+    }).catch(function (err) {
+      that.setData({
+        saving: false
+      })
+      wx.showToast({
+        title: err && err.message ? err.message : '保存失败',
+        icon: 'none'
+      })
+    })
+  },
+
+  getCountryLabel: function () {
+    return findLabelByValue(COUNTRY_OPTIONS, this.data.form.country)
+  },
+
+  getRaceLabel: function () {
+    return findLabelByValue(RACE_OPTIONS, this.data.form.race)
+  },
+
+  getOccupationLabel: function () {
+    return findLabelByValue(OCCUPATION_OPTIONS, this.data.form.occupation)
+  }
+})

+ 3 - 0
packageWellness/pages/tcm/user-form/user-form.json

@@ -0,0 +1,3 @@
+{
+  "navigationBarTitleText": "添加检测人"
+}

+ 67 - 0
packageWellness/pages/tcm/user-form/user-form.wxml

@@ -0,0 +1,67 @@
+<view class="user-form-page">
+  <view class="hero-card">
+    <view class="hero-badge">检测人资料</view>
+    <view class="hero-title">{{isEditMode ? '编辑检测人' : '新增检测人'}}</view>
+    <view class="hero-desc">与 APP 保持一致,支持昵称、性别、生日、身高、目标体重、国家、人种、职业。</view>
+  </view>
+
+  <view class="group-title">基础信息</view>
+
+  <view class="form-card">
+    <view class="form-label">昵称 <text class="required-mark">*</text></view>
+    <input class="form-input" maxlength="20" placeholder="请输入昵称" bindinput="updateNickName" value="{{form.nickName}}" />
+  </view>
+
+  <view class="form-card">
+    <view class="form-label">性别 <text class="required-mark">*</text></view>
+    <picker range="{{sexOptions}}" value="{{sexIndex}}" bindchange="changeSex">
+      <view class="form-picker">{{sexOptions[sexIndex]}}</view>
+    </picker>
+  </view>
+
+  <view class="form-card">
+    <view class="form-label">生日 <text class="required-mark">*</text></view>
+    <picker mode="date" value="{{form.birthday}}" start="1920-01-01" end="2026-12-31" bindchange="updateBirthday">
+      <view class="form-picker">{{form.birthday}}</view>
+    </picker>
+  </view>
+
+  <view class="form-card">
+    <view class="form-label">身高 <text class="required-mark">*</text></view>
+    <picker range="{{heightOptions}}" range-key="label" value="{{heightIndex}}" bindchange="changeHeight">
+      <view class="form-picker">{{heightOptions[heightIndex].label}}</view>
+    </picker>
+  </view>
+
+  <view class="form-card">
+    <view class="form-label">目标体重</view>
+    <picker range="{{targetWeightOptions}}" range-key="label" value="{{targetWeightIndex}}" bindchange="changeTargetWeight">
+      <view class="form-picker">{{targetWeightOptions[targetWeightIndex].label}}</view>
+    </picker>
+  </view>
+
+  <view class="group-title">补充信息</view>
+
+  <view class="form-card">
+    <view class="form-label">国家</view>
+    <picker range="{{countryOptions}}" range-key="label" value="{{countryIndex}}" bindchange="changeCountry">
+      <view class="form-picker">{{countryOptions[countryIndex].label}}</view>
+    </picker>
+  </view>
+
+  <view class="form-card">
+    <view class="form-label">人种</view>
+    <picker range="{{raceOptions}}" range-key="label" value="{{raceIndex}}" bindchange="changeRace">
+      <view class="form-picker">{{raceOptions[raceIndex].label}}</view>
+    </picker>
+  </view>
+
+  <view class="form-card">
+    <view class="form-label">职业</view>
+    <picker range="{{occupationOptions}}" range-key="label" value="{{occupationIndex}}" bindchange="changeOccupation">
+      <view class="form-picker">{{occupationOptions[occupationIndex].label}}</view>
+    </picker>
+  </view>
+
+  <view class="submit-btn {{saving ? 'submit-btn-disabled' : ''}}" bindtap="submitForm">{{saving ? '保存中...' : '保存检测人'}}</view>
+</view>

+ 107 - 0
packageWellness/pages/tcm/user-form/user-form.wxss

@@ -0,0 +1,107 @@
+.user-form-page {
+  min-height: 100%;
+  background: #f7f4ed;
+  padding: 24rpx 24rpx 40rpx;
+  box-sizing: border-box;
+}
+
+.hero-card,
+.form-card {
+  background: #fff;
+  border-radius: 22rpx;
+  box-shadow: 0 10rpx 22rpx rgba(0, 0, 0, 0.05);
+}
+
+.hero-card {
+  padding: 30rpx 28rpx;
+  margin-bottom: 24rpx;
+  background: linear-gradient(135deg, #efe2bd, #eab86a);
+}
+
+.hero-badge {
+  display: inline-block;
+  padding: 8rpx 18rpx;
+  border-radius: 999rpx;
+  background: rgba(255, 255, 255, 0.35);
+  color: #6b5630;
+  font-size: 22rpx;
+}
+
+.hero-title {
+  margin-top: 14rpx;
+  font-size: 38rpx;
+  font-weight: 700;
+  color: #4f3d1c;
+}
+
+.hero-desc {
+  margin-top: 14rpx;
+  font-size: 24rpx;
+  line-height: 1.7;
+  color: #6b5630;
+}
+
+.group-title {
+  margin: 26rpx 8rpx 18rpx;
+  font-size: 30rpx;
+  font-weight: 700;
+  color: #4c4c4c;
+}
+
+.form-card {
+  margin-bottom: 20rpx;
+  padding: 24rpx 26rpx;
+}
+
+.form-label {
+  font-size: 26rpx;
+  color: #666;
+  margin-bottom: 14rpx;
+}
+
+.required-mark {
+  color: #e35b4e;
+}
+
+.form-input,
+.form-picker {
+  width: 100%;
+  min-height: 76rpx;
+  line-height: 76rpx;
+  font-size: 30rpx;
+  color: #2b2b2b;
+}
+
+.form-picker {
+  position: relative;
+  padding-right: 36rpx;
+  box-sizing: border-box;
+}
+
+.form-picker::after {
+  content: '';
+  position: absolute;
+  right: 4rpx;
+  top: 50%;
+  width: 14rpx;
+  height: 14rpx;
+  margin-top: -12rpx;
+  border-right: 3rpx solid #b7802a;
+  border-bottom: 3rpx solid #b7802a;
+  transform: rotate(45deg);
+}
+
+.submit-btn {
+  height: 88rpx;
+  line-height: 88rpx;
+  margin-top: 36rpx;
+  text-align: center;
+  color: #fff;
+  font-size: 30rpx;
+  border-radius: 999rpx;
+  background: linear-gradient(135deg, #efc57b, #d4932b);
+}
+
+.submit-btn-disabled {
+  opacity: 0.6;
+}

+ 33 - 33
pages/user/all/all.wxml

@@ -1,6 +1,6 @@
 <!--登陆界面-->
 <view class="logindiv" wx:if="{{userInfo == 'nologin'}}">
-	<image class="logindiv_image" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/xcx/start.png"></image>
+	<image class="logindiv_image" src="https://cfohow.hiwavo.com/xcx/xcx/start.png"></image>
 	<!-- 旧版弃用 -->
     <!-- <button class="loginbtn" bindtap="getUserProfile">微信授权登陆</button> -->
     <!-- <button class="loginbtn" open-type="getUserInfo" bindgetuserinfo="getUserInfo">微信授权登陆</button> -->
@@ -24,7 +24,7 @@
 
 <view class="user">
   <view class="user-top">
-    <image src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/image/icon/parner.png" class="user-par" wx:if="{{checkData.copartner_state == 1}}" />
+    <image src="https://cfohow.hiwavo.com/xcx/image/icon/parner.png" class="user-par" wx:if="{{checkData.copartner_state == 1}}" />
     <view class="user-name ellipsis">{{userInfo.nickname}}</view>
     
     <button class="user-title" wx:if="{{userInfo == 'nologin'}}" bindtap="getUserProfile" style="font-size: 18px;margin-top: -20px;">立即登陆</button>
@@ -49,22 +49,22 @@
     <view class="user-radishbg">
 		<view class="user-points" bindtap="toSilver">
 		  <view class="user-points__title">我的银豆</view>
-		  <image class="user-points__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+		  <image class="user-points__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
 		  <view class="user-points__num">{{silver / 100}}</view>
 		</view>
       <!-- <view class="user-points" bindtap="toIntegral">
         <view class="user-points__title">我的积分</view>
-        <image class="user-points__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-points__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
         <view class="user-points__num">{{integral / 100}}</view>
       </view> -->
 	  <!-- <view class="user-points" bindtap="toCashList">
 		  <view class="user-points__title">我的收入</view>
-		  <image class="user-points__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+		  <image class="user-points__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
 		  <view class="user-points__num">{{cashbalance / 100}}</view>
 		</view> -->
       <view class="user-points" bindtap="toRadish">
         <view class="user-points__title">我的提货券</view>
-        <image class="user-points__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-points__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
         <view class="user-points__num">{{balance / 100}}</view>
       </view>
       <view class="user-luobo">
@@ -88,32 +88,32 @@
       <navigator url="/packageUser/pages/user/orders/orders" >
         <view class="user-line">
           <view class="user-line__title">我的订单</view>
-          <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+          <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
         </view>
       </navigator>
       -->
       <view class="user-state" bindtap="checkbindtel">
         <navigator url="/packageUser/pages/user/orders/orders" >
           <view class="user-dingdan">
-            <image class="user-paid__icon" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/icon/dingdan.png"></image>
+            <image class="user-paid__icon" src="https://cfohow.hiwavo.com/xcx/icon/dingdan.png"></image>
             <view class="user-paid__text">我的订单</view>
           </view>
         </navigator>
         <navigator url="/packageUser/pages/user/orders/orders?type=unpay" >
           <view class="user-paid">
-            <image class="user-paid__icon" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/paid.png"></image>
+            <image class="user-paid__icon" src="https://cfohow.hiwavo.com/xcx/user/paid.png"></image>
             <view class="user-paid__text">待支付</view>
           </view>
         </navigator>
         <navigator url="/packageUser/pages/user/orders/orders?type=processing" >
           <view class="user-deliver">
-            <image class="user-paid__icon" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/deliver.png"></image>
+            <image class="user-paid__icon" src="https://cfohow.hiwavo.com/xcx/user/deliver.png"></image>
             <view class="user-deliver__text">待发货</view>
           </view>
         </navigator>
         <navigator url="/packageUser/pages/user/orders/orders?type=dispatch" >
           <view class="user-collect">
-            <image class="user-collect__icon" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/collect.png"></image>
+            <image class="user-collect__icon" src="https://cfohow.hiwavo.com/xcx/user/collect.png"></image>
             <view class="user-collect__text">待收货</view>
           </view>
         </navigator>
@@ -124,13 +124,13 @@
     <view class="user-padding">
       <navigator url="/packageUser/pages/user/farm/farm" >
         <view class="user-farm">
-          <image class="user-farm__icon" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/icon/farm.png"></image>
+          <image class="user-farm__icon" src="https://cfohow.hiwavo.com/xcx/icon/farm.png"></image>
           <view class="user-farm__text">我的团购</view>
         </view>
       </navigator>
       <navigator url="/packageUser/pages/user/granary/granary">
         <view class="user-granary">
-          <image class="user-granary__icon" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/icon/granary.png"></image>
+          <image class="user-granary__icon" src="https://cfohow.hiwavo.com/xcx/icon/granary.png"></image>
           <view class="user-granary__text">我的粮仓</view>
         </view>
       </navigator>
@@ -142,73 +142,73 @@
 		<!--
 		<view class="user-line" bindtap="toPromotion">
 		  <view class="user-line__title">我的赠券</view>
-		  <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+		  <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
 		</view>
 		-->
       <!-- <view class="user-line" bindtap="toInviteList">
         <view class="user-line__title">我的推广</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view> -->
       <view class="user-line" bindtap="toCash" style="border:0;" wx:if="{{userInfo.show_invite_mode}}">
         <view class="user-line__title">我的收入</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view>
 	  <view class="user-line" bindtap="toCommission" style="border:0;" wx:if="{{userInfo.tc_bl>0}}">
 	    <view class="user-line__title">我的提成</view>
-	    <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+	    <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
 	  </view>
     </view>
     <view class="user-padding">
       <!-- <view class="user-line" bindtap="toBindOldSys" wx:if="{{info.wx_user.can_bind}}">
         <view class="user-line__title">旧系统用户绑定</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view> -->
       
       <!-- <view class="user-line" bindtap="toIdentity">
         <view class="user-line__title">会员认证</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view> -->
       <!-- <view class="user-line" bindtap="toBankInfo">
         <view class="user-line__title">银行信息</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view> -->
       <view class="user-line" bindtap="toAddressList">
         <view class="user-line__title">收货地址</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view>
 	  <view class="user-line" bindtap="toCanteen" wx:if="{{info['user']['employ']}}">
 	    <view class="user-line__title">员工订餐</view>
-	    <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+	    <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
 	  </view>
       <!--<view class="user-line" bindtap="toSafe" >
         <view class="user-line__title">修改交易密码</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view>-->
       <!--
       <view class="user-line" bindtap="toApplyLeader" >
         <view class="user-line__title">团长申请</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view>
       <view class="user-line" bindtap="toAreaCart" >
         <view class="user-line__title">店铺专区</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view>
       -->
       <!-- <view class="user-line" bindtap="toProxy" wx:if="{{userInfo.rank == 3}}">
         <view class="user-line__title">我的代理</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view> -->
      <!-- <view class="user-line" bindtap="toCustom" >
         <view class="user-line__title">联系客服</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view> -->
 	 <button class="user-line" open-type="contact" >
         <view class="user-line__title">联系客服</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </button>
 	  <!-- <button class="user-line" bindtap="tiggerQR" >
 	    <view class="user-line__title">联系客服</view>
-	    <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+	    <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
 	  </button>-->
 	  <view class="kfqr-dialog" wx:if="{{showqr}}" bindtap="tiggerQR">
 	  	<view class="kfqr" catchtap="catchtapQR">
@@ -219,7 +219,7 @@
       <!--
       <view class="user-line" bindtap="toAbout" style="border:0;">
         <view class="user-line__title">关于商城</view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view>
       -->
     </view>
@@ -227,19 +227,19 @@
       <view class="user-line" bindtap="toMerchant" style="{{checkData.is_super_admin ? 'margin-top: 20rpx;':'margin-top: 20rpx;border:0;'}}">
         <view class="user-line__title">订单管理</view>
         <view class="user-line__merchant"></view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
         <view class="user-line__merchantNum" wx:if="{{merchant}}">{{merchant.list_count}}</view>
       </view>
       <view class="user-line" bindtap="toMerchant1" style="border:0;" wx:if="{{checkData.is_super_admin}}">
         <view class="user-line__title">结算管理</view>
         <view class="user-line__merchant1"></view>
-        <image class="user-line__right" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/right.png"></image>
+        <image class="user-line__right" src="https://cfohow.hiwavo.com/xcx/user/right.png"></image>
       </view>
     </view> -->
   </view>
   <!--
   <view class="unbind" wx:if="{{!bindTel}}">
-    <image class="unbind-img" src="http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/bindtel.png"></image>
+    <image class="unbind-img" src="https://cfohow.hiwavo.com/xcx/user/bindtel.png"></image>
     <view class="unbind-title">你还没绑定手机号</view>
     <view class="unbind-about">兑换产品前要确定绑定了手机哦!</view>
     <view class="unbind-btn" bindtap="toBindTel">去绑定</view>

+ 2 - 2
pages/user/all/all.wxss

@@ -267,7 +267,7 @@
   margin-top: 30rpx;
   margin-left: 18rpx;
   background-size: 100% 100%;
-  background-image: url('http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/user/merchant.png');
+  background-image: url('https://cfohow.hiwavo.com/xcx/user/merchant.png');
 }
 .user-line__merchant1 {
   float: left;
@@ -276,7 +276,7 @@
   margin-top: 30rpx;
   margin-left: 18rpx;
   background-size: 100% 100%;
-  background-image: url('http://fohow.oss-cn-shenzhen.aliyuncs.com/xcx/icon/account_icon.jpg');
+  background-image: url('https://cfohow.hiwavo.com/xcx/icon/account_icon.jpg');
 }
 .user-line__merchantNum {
   float: right;

+ 125 - 0
pages/wellness/wellness.js

@@ -0,0 +1,125 @@
+var tcm = require('../../utils/tcm.js')
+
+Page({
+  data: {
+    currentUser: null,
+    latestExam: null,
+    latestExamSummary: '',
+    latestExamStatusText: '',
+    loadingUser: true,
+    loadingLive: false
+  },
+
+  onShow: function () {
+    this.loadPageData()
+  },
+
+  loadPageData: function () {
+    this.loadScaleUsers()
+  },
+
+  loadScaleUsers: function () {
+    var that = this
+    that.setData({
+      loadingUser: true
+    })
+
+    tcm.getScaleUsers().then(function (list) {
+      var scaleUsers = Array.isArray(list) ? list : []
+      var currentUser = null
+
+      if (scaleUsers.length > 0) {
+        currentUser = scaleUsers[0]
+        for (var i = 0; i < scaleUsers.length; i++) {
+          if (scaleUsers[i].isDefault === true || scaleUsers[i].isDefault === 1) {
+            currentUser = scaleUsers[i]
+            break
+          }
+          if (scaleUsers[i].active === true && !currentUser) {
+            currentUser = scaleUsers[i]
+          }
+        }
+      }
+
+      that.setData({
+        currentUser: currentUser,
+        loadingUser: false
+      })
+
+      if (currentUser && currentUser.ID) {
+        that.loadLatestExam(currentUser.ID)
+      } else {
+        that.setData({
+          latestExam: null,
+          latestExamSummary: '',
+          latestExamStatusText: '添加检测人后即可开始 AI 舌诊'
+        })
+      }
+    }).catch(function () {
+      that.setData({
+        loadingUser: false,
+        latestExam: null,
+        latestExamSummary: '',
+        latestExamStatusText: '暂未获取到康养档案'
+      })
+    })
+  },
+
+  loadLatestExam: function (scaleUserId) {
+    var that = this
+    tcm.getLatestExam(scaleUserId).then(function (data) {
+      var exam = data && data.exam ? data.exam : null
+      var aiSections = data && data.aiSections ? data.aiSections : {}
+      that.setData({
+        latestExam: exam,
+        latestExamSummary: that.getLatestExamSummary(aiSections),
+        latestExamStatusText: that.formatExamStatus(exam ? exam.status : '')
+      })
+    }).catch(function () {
+      that.setData({
+        latestExam: null,
+        latestExamSummary: '',
+        latestExamStatusText: '暂无诊疗报告'
+      })
+    })
+  },
+
+  formatExamStatus: function (status) {
+    if (status === 'completed') {
+      return '最新报告已生成'
+    }
+    if (status === 'pending' || status === 'generating') {
+      return '最新报告生成中'
+    }
+    if (status === 'failed') {
+      return '上次诊疗生成失败,可重新检测'
+    }
+    return '暂无诊疗报告'
+  },
+
+  getLatestExamSummary: function (aiSections) {
+    if (!aiSections) {
+      return ''
+    }
+    return aiSections.summary || aiSections.general || aiSections.observation || ''
+  },
+
+  goDiagnosis: function () {
+    wx.navigateTo({
+      url: '/packageWellness/pages/tcm/diagnosis/diagnosis'
+    })
+  },
+
+  goLatestReport: function () {
+    if (!this.data.currentUser || !this.data.currentUser.ID) {
+      wx.showToast({
+        title: '请先添加检测人',
+        icon: 'none'
+      })
+      return
+    }
+    wx.navigateTo({
+      url: '/packageWellness/pages/tcm/report/report?scaleUserId=' + this.data.currentUser.ID
+    })
+  }
+})

+ 3 - 0
pages/wellness/wellness.json

@@ -0,0 +1,3 @@
+{
+  "navigationBarTitleText": "康养"
+}

+ 53 - 0
pages/wellness/wellness.wxml

@@ -0,0 +1,53 @@
+<view class="wellness-page">
+  <view class="hero">
+    <view class="hero-head">
+      <image class="hero-icon" src="/images/footer/wellnessc.png" mode="aspectFit"></image>
+      <view class="hero-badge">康养中心</view>
+    </view>
+    <view class="hero-title">AI舌诊 · 中医诊疗</view>
+    <view class="hero-user" wx:if="{{currentUser}}">
+      当前检测人:{{currentUser.nickName}} · {{currentUser.sex == 1 ? '女' : '男'}} · {{currentUser.height || '--'}}cm
+    </view>
+    <view class="hero-user" wx:else>当前检测人:未设置</view>
+  </view>
+
+  <view class="summary-card" bindtap="goLatestReport">
+    <view class="summary-left">
+      <view class="summary-title">康养档案</view>
+      <view class="summary-status">{{latestExamStatusText}}</view>
+      <view class="summary-text" wx:if="{{latestExamSummary}}">{{latestExamSummary}}</view>
+      <view class="summary-time" wx:if="{{latestExam && latestExam.CreatedAt}}">最近检测:{{latestExam.CreatedAt}}</view>
+      <view class="summary-time" wx:else>查看最新检测人的舌诊报告</view>
+    </view>
+    <view class="summary-action">查看报告</view>
+  </view>
+
+  <view class="section-title">核心服务</view>
+
+  <view class="feature-card primary" bindtap="goDiagnosis">
+    <view class="feature-top">
+      <view class="feature-main">
+        <view class="feature-tag">AI 智能分析</view>
+        <view class="feature-title">中医诊疗</view>
+        <view class="feature-subtitle">舌面、舌下、面部上传 + 问诊生成报告</view>
+      </view>
+      <view class="feature-icon">
+        <image class="feature-icon-image" src="/images/footer/wellnessc.png" mode="aspectFit"></image>
+      </view>
+    </view>
+    <view class="feature-points">
+      <view class="feature-point">3 张关键照片</view>
+      <view class="feature-point">全身照选填</view>
+      <view class="feature-point">动态问诊</view>
+      <view class="feature-point">AI 调养建议</view>
+    </view>
+    <view class="feature-btn">立即检测</view>
+  </view>
+
+  <view class="section-title">检测须知</view>
+  <view class="tips-card">
+    <view class="tips-item">自然光或均匀灯光下拍摄,避免逆光和强曝光。</view>
+    <view class="tips-item">保持镜头稳定,对焦清晰,确保舌面纹理可见。</view>
+    <view class="tips-item">AI 报告仅供健康参考,不替代专业医疗诊断。</view>
+  </view>
+</view>

+ 231 - 0
pages/wellness/wellness.wxss

@@ -0,0 +1,231 @@
+.wellness-page {
+  min-height: 100%;
+  background: #f7f4ed;
+  padding: 24rpx;
+  box-sizing: border-box;
+}
+
+.hero {
+  background: linear-gradient(135deg, #efe2bd, #eab86a);
+  border-radius: 24rpx;
+  padding: 34rpx 30rpx;
+  color: #5b4724;
+  box-shadow: 0 16rpx 30rpx rgba(212, 147, 43, 0.15);
+}
+
+.hero-head {
+  display: flex;
+  align-items: center;
+}
+
+.hero-icon {
+  width: 44rpx;
+  height: 44rpx;
+  margin-right: 12rpx;
+}
+
+.hero-badge {
+  display: inline-block;
+  padding: 8rpx 20rpx;
+  border-radius: 999rpx;
+  background: rgba(255, 255, 255, 0.45);
+  font-size: 24rpx;
+  margin-bottom: 18rpx;
+}
+
+.hero-title {
+  font-size: 42rpx;
+  font-weight: bold;
+  color: #4f3d1c;
+}
+
+.hero-user {
+  margin-top: 18rpx;
+  font-size: 24rpx;
+  color: #6b5630;
+}
+
+.summary-card,
+.feature-card,
+.tips-card {
+  background: #fff;
+  border-radius: 22rpx;
+  box-shadow: 0 10rpx 24rpx rgba(0, 0, 0, 0.05);
+}
+
+.summary-card {
+  margin-top: 22rpx;
+  padding: 28rpx;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.summary-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #2b2b2b;
+}
+
+.summary-status {
+  margin-top: 10rpx;
+  font-size: 26rpx;
+  color: #d4932b;
+}
+
+.summary-time {
+  margin-top: 8rpx;
+  font-size: 22rpx;
+  color: #999;
+}
+
+.summary-text {
+  margin-top: 12rpx;
+  font-size: 24rpx;
+  line-height: 1.7;
+  color: #666;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
+
+.summary-action {
+  width: 150rpx;
+  height: 62rpx;
+  border-radius: 999rpx;
+  background: linear-gradient(135deg, #efc57b, #d4932b);
+  color: #fff;
+  text-align: center;
+  line-height: 62rpx;
+  font-size: 24rpx;
+}
+
+.section-title {
+  margin: 28rpx 8rpx 18rpx;
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #4c4c4c;
+}
+
+.feature-card {
+  padding: 28rpx;
+  margin-bottom: 22rpx;
+}
+
+.feature-card.primary {
+  background: linear-gradient(180deg, #fffaf1, #ffffff);
+  border: 1rpx solid #f0dfbc;
+}
+
+.feature-top {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+}
+
+.feature-main {
+  flex: 1;
+  padding-right: 18rpx;
+}
+
+.feature-tag {
+  display: inline-block;
+  padding: 6rpx 16rpx;
+  border-radius: 999rpx;
+  background: #f7eedc;
+  color: #b7802a;
+  font-size: 22rpx;
+}
+
+.feature-title {
+  margin-top: 16rpx;
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #2b2b2b;
+}
+
+.feature-subtitle {
+  margin-top: 12rpx;
+  font-size: 24rpx;
+  line-height: 1.7;
+  color: #666;
+}
+
+.feature-icon {
+  width: 92rpx;
+  height: 92rpx;
+  box-sizing: border-box;
+  border-radius: 24rpx;
+  background: rgba(234, 184, 106, 0.16);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.feature-icon-image {
+  width: 54rpx;
+  height: 54rpx;
+}
+
+.feature-points {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: 22rpx;
+}
+
+.feature-point {
+  margin-right: 14rpx;
+  margin-bottom: 14rpx;
+  padding: 10rpx 18rpx;
+  background: #f8f5ef;
+  border-radius: 999rpx;
+  font-size: 22rpx;
+  color: #7d6a46;
+}
+
+.feature-btn {
+  width: 220rpx;
+  height: 68rpx;
+  border-radius: 999rpx;
+  margin-top: 10rpx;
+  background: linear-gradient(135deg, #efc57b, #d4932b);
+  color: #fff;
+  text-align: center;
+  line-height: 68rpx;
+  font-size: 26rpx;
+}
+
+.feature-btn.secondary {
+  background: #efe7d5;
+  color: #b7802a;
+}
+
+.tips-card {
+  padding: 26rpx 28rpx;
+  margin-bottom: 24rpx;
+}
+
+.tips-item {
+  position: relative;
+  padding-left: 24rpx;
+  margin-bottom: 16rpx;
+  font-size: 24rpx;
+  line-height: 1.7;
+  color: #666;
+}
+
+.tips-item:last-child {
+  margin-bottom: 0;
+}
+
+.tips-item::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 14rpx;
+  width: 10rpx;
+  height: 10rpx;
+  border-radius: 50%;
+  background: #eab86a;
+}

+ 5 - 5
utils/request.js

@@ -1,14 +1,12 @@
-// 新正式(未审批)不用了
-// const apiHost = 'https://fohowapi.hiwavo.com/';
-// const apiHostX = 'https://xj.hiwavo.com/';//正式
+
 
 // 新正式
 const apiHost = 'https://xcx-api.fohowyc.com/';
 const apiHostX = 'https://app-api.fohowyc.com/';//正式
 
 // 测试
-// const apiHost = 'https://tfohowapi.hiwavo.com/';
-// const apiHostX = 'https://txj.hiwavo.com/';
+ //const apiHost = 'https://tfohowapi.hiwavo.com/';
+ //const apiHostX = 'https://txj.hiwavo.com/';
 
 
 
@@ -450,6 +448,8 @@ module.exports = {
   $put: $put,
   $get: $get,
   $del: $del,
+  apiHost: apiHost,
+  apiHostX: apiHostX,
   login: login,
   getAuthorize: getAuthorize,
   updateWxUserInfo: updateWxUserInfo,

+ 255 - 0
utils/tcm.js

@@ -0,0 +1,255 @@
+var requestUtils = require('./request.js')
+
+const apiHost = requestUtils.apiHost || 'https://xcx-api.fohowyc.com/'
+const appApiHost = requestUtils.apiHostX || 'https://app-api.fohowyc.com/'
+
+function getLanguageCode () {
+  return 'zh'
+}
+
+function getToken () {
+  return wx.getStorageSync('lbt_token_key') || ''
+}
+
+function getHeaders (extra) {
+  var token = getToken()
+  var headers = {
+    'Accept-Language': getLanguageCode(),
+    terminal: 'mini-program'
+  }
+
+  if (token) {
+    headers.Authorization = 'Bearer ' + token
+  }
+
+  if (extra) {
+    for (var key in extra) {
+      headers[key] = extra[key]
+    }
+  }
+
+  return headers
+}
+
+function normalizeResponse (res) {
+  return res && res.data ? res.data : res
+}
+
+function getSuccessCode (payload) {
+  if (payload && payload.code !== undefined) {
+    return payload.code
+  }
+  if (payload && payload.Code !== undefined) {
+    return payload.Code
+  }
+  return undefined
+}
+
+function getResponseData (payload) {
+  if (!payload) {
+    return payload
+  }
+  if (payload.data !== undefined) {
+    return payload.data
+  }
+  if (payload.Data !== undefined) {
+    return payload.Data
+  }
+  return payload
+}
+
+function getErrorMessage (payload) {
+  if (!payload) {
+    return '请求失败'
+  }
+  return payload.msg || payload.Msg || payload.err_msg || payload.message || '请求失败'
+}
+
+function request (options) {
+  var host = options.host === 'old' ? apiHost : appApiHost
+  var method = options.method || 'GET'
+  return new Promise(function (resolve, reject) {
+    wx.request({
+      url: host + options.path,
+      method: method,
+      data: options.data || {},
+      header: getHeaders(options.header || {
+        'content-type': method === 'GET' ? 'application/json' : 'application/json'
+      }),
+      success: function (res) {
+        var payload = normalizeResponse(res)
+        var code = getSuccessCode(payload)
+        if (res.statusCode >= 200 && res.statusCode < 300 && (code === undefined || code === 0)) {
+          resolve(getResponseData(payload))
+          return
+        }
+        reject({
+          statusCode: res.statusCode,
+          payload: payload,
+          message: getErrorMessage(payload)
+        })
+      },
+      fail: function (err) {
+        reject({
+          message: '网络异常,请稍后重试',
+          error: err
+        })
+      }
+    })
+  })
+}
+
+function uploadObservationImage (filePath, kind) {
+  return new Promise(function (resolve, reject) {
+    wx.uploadFile({
+      url: appApiHost + 'tcm/observation-images',
+      filePath: filePath,
+      name: 'file',
+      formData: {
+        kind: kind
+      },
+      header: getHeaders(),
+      success: function (res) {
+        var payload = {}
+        try {
+          payload = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
+        } catch (e) {
+          reject({
+            message: '上传返回解析失败',
+            error: e
+          })
+          return
+        }
+
+        if (res.statusCode >= 200 && res.statusCode < 300 && getSuccessCode(payload) === 0) {
+          resolve(getResponseData(payload))
+          return
+        }
+
+        reject({
+          statusCode: res.statusCode,
+          payload: payload,
+          message: getErrorMessage(payload)
+        })
+      },
+      fail: function (err) {
+        reject({
+          message: '上传失败,请稍后重试',
+          error: err
+        })
+      }
+    })
+  })
+}
+
+function getCurrentUserInfo () {
+  return request({
+    host: 'old',
+    path: 'v1/user/info'
+  })
+}
+
+function getScaleUsers () {
+  return getCurrentUserInfo().then(function (info) {
+    var user = info && info.user ? info.user : {}
+    var userId = user.user_id || user.id
+    if (!userId) {
+      return []
+    }
+    return request({
+      path: 'scaleUsers/getScaleUsersList?ID=' + userId + '&page=1&pageSize=999'
+    }).then(function (data) {
+      if (Array.isArray(data)) {
+        return data
+      }
+      if (data && Array.isArray(data.list)) {
+        return data.list
+      }
+      return []
+    })
+  })
+}
+
+function getPhotoTips () {
+  return request({
+    path: 'tcm/photo-tips'
+  })
+}
+
+function getQuestions () {
+  return request({
+    path: 'tcm/questions'
+  })
+}
+
+function submitExam (data) {
+  return request({
+    path: 'tcm/exams',
+    method: 'POST',
+    data: data
+  })
+}
+
+function getLatestExam (scaleUserId) {
+  return request({
+    path: 'tcm/exams/latest?scaleUserId=' + scaleUserId
+  })
+}
+
+function getExamDetail (examId) {
+  return request({
+    path: 'tcm/exams/' + examId
+  })
+}
+
+function createScaleUser (data) {
+  return request({
+    path: 'scaleUsers/createScaleUsers',
+    method: 'POST',
+    data: data
+  })
+}
+
+function updateScaleUser (data) {
+  return request({
+    path: 'scaleUsers/updateScaleUsers',
+    method: 'PUT',
+    data: data
+  })
+}
+
+function setDefaultScaleUser (id) {
+  return request({
+    path: 'scaleUsers/SetDefaultUser',
+    method: 'PUT',
+    data: {
+      id: id
+    }
+  })
+}
+
+function deleteScaleUser (id) {
+  return request({
+    path: 'scaleUsers/deleteScaleUsers',
+    method: 'DELETE',
+    data: {
+      ID: id
+    }
+  })
+}
+
+module.exports = {
+  getLanguageCode: getLanguageCode,
+  getCurrentUserInfo: getCurrentUserInfo,
+  getScaleUsers: getScaleUsers,
+  getPhotoTips: getPhotoTips,
+  getQuestions: getQuestions,
+  uploadObservationImage: uploadObservationImage,
+  submitExam: submitExam,
+  getLatestExam: getLatestExam,
+  getExamDetail: getExamDetail,
+  createScaleUser: createScaleUser,
+  updateScaleUser: updateScaleUser,
+  setDefaultScaleUser: setDefaultScaleUser,
+  deleteScaleUser: deleteScaleUser
+}

+ 0 - 1
wx84f6c33a9fa3977d.o6zAJs_N7Pl-AGqQoHXByLv0gppo.cdaiXNvNJvtIccfb3d8c25d8b371192b6521c76946f9.jpg

@@ -1 +0,0 @@
-{"errcode":41030,"errmsg":"invalid page hint: [EHJcbkylRa-gk0Dta]"}