|
|
@@ -0,0 +1,266 @@
|
|
|
+<template>
|
|
|
+ <view class="tongue-container">
|
|
|
+ <!-- 步骤条 -->
|
|
|
+ <view class="step-bar">
|
|
|
+ <view class="step-item" :class="{ active: step >= 1, done: step > 1 }">
|
|
|
+ <text class="step-num">{{ step > 1 ? '✓' : '1' }}</text>
|
|
|
+ <text class="step-label">选择成员</text>
|
|
|
+ </view>
|
|
|
+ <view class="step-line" :class="{ active: step > 1 }"></view>
|
|
|
+ <view class="step-item" :class="{ active: step >= 2, done: step > 2 }">
|
|
|
+ <text class="step-num">{{ step > 2 ? '✓' : '2' }}</text>
|
|
|
+ <text class="step-label">舌象拍照</text>
|
|
|
+ </view>
|
|
|
+ <view class="step-line" :class="{ active: step > 2 }"></view>
|
|
|
+ <view class="step-item" :class="{ active: step >= 3 }">
|
|
|
+ <text class="step-num">3</text>
|
|
|
+ <text class="step-label">预览确认</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- Step 1: 选成员 -->
|
|
|
+ <view class="step-content" v-if="step === 1">
|
|
|
+ <view class="section-title">选择家庭成员</view>
|
|
|
+ <view class="member-list">
|
|
|
+ <view class="member-item"
|
|
|
+ v-for="m in familyMembers"
|
|
|
+ :key="m.id"
|
|
|
+ :class="{ selected: selectedMember && selectedMember.id === m.id }"
|
|
|
+ @click="selectMember(m)">
|
|
|
+ <text class="member-avatar">{{ m.name && m.name.slice(0, 1) }}</text>
|
|
|
+ <text class="member-name">{{ m.name }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <button class="btn-primary" @click="step = 2" :disabled="!selectedMember">下一步</button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- Step 2: 拍照 -->
|
|
|
+ <view class="step-content" v-if="step === 2">
|
|
|
+ <view class="section-title">拍摄舌象</view>
|
|
|
+ <view class="photo-area">
|
|
|
+ <view class="photo-placeholder" v-if="!photoPath" @click="takePhoto">
|
|
|
+ <text class="photo-icon">📷</text>
|
|
|
+ <text class="photo-text">点击拍摄舌象</text>
|
|
|
+ <text class="photo-sub">提示:自然光下拍摄,避免有色光线</text>
|
|
|
+ </view>
|
|
|
+ <image class="photo-preview" v-else :src="photoPath" mode="aspectFit" @click="takePhoto"></image>
|
|
|
+ </view>
|
|
|
+ <view class="btn-row">
|
|
|
+ <button class="btn-secondary" @click="step = 1">上一步</button>
|
|
|
+ <button class="btn-primary" @click="uploadAndAnalyze" :disabled="!photoPath || uploading">
|
|
|
+ {{ uploading ? '分析中...' : '开始分析' }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- Step 3: 预览确认 -->
|
|
|
+ <view class="step-content" v-if="step === 3">
|
|
|
+ <view class="result-header">
|
|
|
+ <image class="result-img" :src="tongueResult.imageUrl" mode="aspectFit"></image>
|
|
|
+ <text class="result-assessment">{{ tongueResult.overallAssessment }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="indicator-list">
|
|
|
+ <view class="indicator-item" v-for="(ind, idx) in indicators" :key="idx">
|
|
|
+ <text class="indicator-label">{{ ind.name }}</text>
|
|
|
+ <picker mode="selector" :range="ind.options" :value="ind.selectedIndex" @change="onIndicatorChange($event, idx)">
|
|
|
+ <view class="indicator-value">
|
|
|
+ <text>{{ ind.value }}</text>
|
|
|
+ <text class="indicator-arrow">▼</text>
|
|
|
+ </view>
|
|
|
+ </picker>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="btn-row">
|
|
|
+ <button class="btn-secondary" @click="discardAndBack">放弃</button>
|
|
|
+ <button class="btn-primary" @click="confirmSave" :disabled="saving">
|
|
|
+ {{ saving ? '保存中...' : '确认入库' }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ data: function() {
|
|
|
+ return {
|
|
|
+ step: 1,
|
|
|
+ familyMembers: [],
|
|
|
+ selectedMember: null,
|
|
|
+ photoPath: null,
|
|
|
+ uploading: false,
|
|
|
+ saving: false,
|
|
|
+ tongueResult: {
|
|
|
+ recordId: null,
|
|
|
+ imageUrl: '',
|
|
|
+ overallAssessment: ''
|
|
|
+ },
|
|
|
+ indicators: []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad: function() {
|
|
|
+ this.loadFamilyMembers()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ loadFamilyMembers: function() {
|
|
|
+ var members = uni.getStorageSync('familyChildren') || []
|
|
|
+ if (members.length > 0) {
|
|
|
+ this.familyMembers = members
|
|
|
+ }
|
|
|
+ },
|
|
|
+ selectMember: function(m) {
|
|
|
+ this.selectedMember = m
|
|
|
+ },
|
|
|
+ takePhoto: function() {
|
|
|
+ var self = this
|
|
|
+ uni.chooseImage({
|
|
|
+ count: 1,
|
|
|
+ sourceType: ['camera', 'album'],
|
|
|
+ success: function(res) {
|
|
|
+ self.photoPath = res.tempFilePaths[0]
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ uploadAndAnalyze: function() {
|
|
|
+ if (!this.photoPath) return
|
|
|
+ this.uploading = true
|
|
|
+
|
|
|
+ var self = this
|
|
|
+ uni.uploadFile({
|
|
|
+ url: getApp().globalData.baseUrl + '/api/health/report/parse-preview?type=tongue&memberId=' + this.selectedMember.id,
|
|
|
+ filePath: this.photoPath,
|
|
|
+ name: 'file',
|
|
|
+ success: function(res) {
|
|
|
+ var data = JSON.parse(res.data)
|
|
|
+ if (data.code === 0 && data.data) {
|
|
|
+ self.tongueResult.recordId = data.data.recordId
|
|
|
+ self.tongueResult.imageUrl = data.data.imageUrl
|
|
|
+ self.tongueResult.overallAssessment = data.data.overallAssessment
|
|
|
+ var inds = (data.data.indicators || []).map(function(ind) {
|
|
|
+ var opts = []
|
|
|
+ try { opts = JSON.parse(ind.options) } catch(e) {}
|
|
|
+ return {
|
|
|
+ code: ind.code,
|
|
|
+ name: ind.name,
|
|
|
+ value: ind.value,
|
|
|
+ options: opts,
|
|
|
+ selectedIndex: opts.indexOf(ind.value)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ self.indicators = inds
|
|
|
+ self.step = 3
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: data.message || '分析失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: function() {
|
|
|
+ uni.showToast({ title: '上传失败', icon: 'none' })
|
|
|
+ },
|
|
|
+ complete: function() {
|
|
|
+ self.uploading = false
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ onIndicatorChange: function(e, idx) {
|
|
|
+ var i = parseInt(e.detail.value)
|
|
|
+ this.indicators[idx].selectedIndex = i
|
|
|
+ this.indicators[idx].value = this.indicators[idx].options[i]
|
|
|
+ },
|
|
|
+ confirmSave: function() {
|
|
|
+ this.saving = true
|
|
|
+ var self = this
|
|
|
+ var modifiedIndicators = this.indicators.map(function(ind) {
|
|
|
+ return { code: ind.code, value: ind.value }
|
|
|
+ })
|
|
|
+ uni.request({
|
|
|
+ url: getApp().globalData.baseUrl + '/api/health/report/confirm?type=tongue',
|
|
|
+ method: 'POST',
|
|
|
+ data: {
|
|
|
+ recordId: this.tongueResult.recordId,
|
|
|
+ indicators: modifiedIndicators
|
|
|
+ },
|
|
|
+ success: function(res) {
|
|
|
+ if (res.data.code === 0) {
|
|
|
+ uni.showToast({ title: '舌诊结果已保存' })
|
|
|
+ setTimeout(function() {
|
|
|
+ uni.navigateBack()
|
|
|
+ }, 1500)
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.data.message || '保存失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: function() {
|
|
|
+ uni.showToast({ title: '网络错误', icon: 'none' })
|
|
|
+ },
|
|
|
+ complete: function() {
|
|
|
+ self.saving = false
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ discardAndBack: function() {
|
|
|
+ var self = this
|
|
|
+ uni.showModal({
|
|
|
+ title: '提示',
|
|
|
+ content: '确定放弃本次舌诊分析吗?',
|
|
|
+ success: function(res) {
|
|
|
+ if (res.confirm) {
|
|
|
+ uni.request({
|
|
|
+ url: getApp().globalData.baseUrl + '/api/health/report/discard?type=tongue',
|
|
|
+ method: 'POST',
|
|
|
+ data: { recordId: self.tongueResult.recordId }
|
|
|
+ })
|
|
|
+ uni.navigateBack()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.tongue-container { padding: 30rpx; background: #F5FAFE; min-height: 100vh; }
|
|
|
+.step-bar { display: flex; align-items: center; margin-bottom: 40rpx; }
|
|
|
+.step-item { display: flex; flex-direction: column; align-items: center; }
|
|
|
+.step-num { width: 48rpx; height: 48rpx; border-radius: 50%; background: #E2E8F0; color: #94A3B8; text-align: center; line-height: 48rpx; font-size: 24rpx; font-weight: bold; }
|
|
|
+.step-item.active .step-num { background: #F97316; color: #fff; }
|
|
|
+.step-item.done .step-num { background: #10B981; color: #fff; }
|
|
|
+.step-label { font-size: 22rpx; color: #94A3B8; margin-top: 8rpx; }
|
|
|
+.step-item.active .step-label { color: #F97316; font-weight: bold; }
|
|
|
+.step-line { flex: 1; height: 2rpx; background: #E2E8F0; margin: 0 16rpx; margin-bottom: 40rpx; }
|
|
|
+.step-line.active { background: #10B981; }
|
|
|
+
|
|
|
+.section-title { font-size: 32rpx; font-weight: bold; color: #1E293B; margin-bottom: 24rpx; }
|
|
|
+.member-list { display: flex; flex-wrap: wrap; gap: 20rpx; margin-bottom: 40rpx; }
|
|
|
+.member-item { width: 140rpx; height: 160rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #fff; border-radius: 16rpx; border: 2rpx solid #E2E8F0; }
|
|
|
+.member-item.selected { border-color: #F97316; background: #FFF7ED; }
|
|
|
+.member-avatar { width: 64rpx; height: 64rpx; border-radius: 50%; background: #FED7AA; text-align: center; line-height: 64rpx; font-size: 28rpx; color: #9A3412; margin-bottom: 8rpx; }
|
|
|
+.member-name { font-size: 24rpx; color: #1E293B; }
|
|
|
+
|
|
|
+.photo-area { display: flex; justify-content: center; margin-bottom: 40rpx; }
|
|
|
+.photo-placeholder { width: 400rpx; height: 400rpx; border: 4rpx dashed #CBD5E1; border-radius: 24rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #fff; }
|
|
|
+.photo-icon { font-size: 80rpx; margin-bottom: 16rpx; }
|
|
|
+.photo-text { font-size: 28rpx; color: #64748B; }
|
|
|
+.photo-sub { font-size: 22rpx; color: #94A3B8; margin-top: 12rpx; }
|
|
|
+.photo-preview { width: 400rpx; height: 400rpx; border-radius: 24rpx; }
|
|
|
+
|
|
|
+.result-header { display: flex; flex-direction: column; align-items: center; margin-bottom: 30rpx; }
|
|
|
+.result-img { width: 300rpx; height: 300rpx; border-radius: 16rpx; margin-bottom: 16rpx; }
|
|
|
+.result-assessment { font-size: 26rpx; color: #475569; text-align: center; line-height: 1.6; }
|
|
|
+
|
|
|
+.indicator-list { margin-bottom: 40rpx; }
|
|
|
+.indicator-item { display: flex; justify-content: space-between; align-items: center; padding: 24rpx 20rpx; background: #fff; border-radius: 12rpx; margin-bottom: 12rpx; }
|
|
|
+.indicator-label { font-size: 28rpx; color: #1E293B; }
|
|
|
+.indicator-value { display: flex; align-items: center; color: #F97316; font-size: 28rpx; }
|
|
|
+.indicator-arrow { font-size: 20rpx; margin-left: 8rpx; }
|
|
|
+
|
|
|
+.btn-row { display: flex; gap: 20rpx; }
|
|
|
+.btn-primary { flex: 1; background: #F97316; color: #fff; border-radius: 40rpx; height: 88rpx; line-height: 88rpx; text-align: center; font-size: 30rpx; }
|
|
|
+.btn-primary:disabled { opacity: 0.5; }
|
|
|
+.btn-secondary { flex: 1; background: #fff; color: #64748B; border-radius: 40rpx; height: 88rpx; line-height: 88rpx; text-align: center; font-size: 30rpx; border: 2rpx solid #E2E8F0; }
|
|
|
+
|
|
|
+.loading-wrap { display: flex; justify-content: center; align-items: center; min-height: 200rpx; }
|
|
|
+.loading-text { color: #94A3B8; font-size: 28rpx; }
|
|
|
+</style>
|