|
@@ -1,286 +1,12 @@
|
|
|
<template>
|
|
<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-item self-first" @click="selectSelf">
|
|
|
|
|
- <text class="member-avatar self-avatar">我</text>
|
|
|
|
|
- <text class="member-name">我自己</text>
|
|
|
|
|
- </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="!canProceed">下一步</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>
|
|
|
|
|
|
|
+ <view></view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
|
|
+// 废弃 — 舌诊功能已迁移到 report-upload?reportType=tongue
|
|
|
export default {
|
|
export default {
|
|
|
- data: function() {
|
|
|
|
|
- return {
|
|
|
|
|
- step: 1,
|
|
|
|
|
- familyMembers: [],
|
|
|
|
|
- selectedMember: null,
|
|
|
|
|
- selectedSelf: true,
|
|
|
|
|
- photoPath: null,
|
|
|
|
|
- uploading: false,
|
|
|
|
|
- saving: false,
|
|
|
|
|
- tongueResult: {
|
|
|
|
|
- recordId: null,
|
|
|
|
|
- imageUrl: '',
|
|
|
|
|
- overallAssessment: ''
|
|
|
|
|
- },
|
|
|
|
|
- indicators: []
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- computed: {
|
|
|
|
|
- canProceed: function() {
|
|
|
|
|
- return this.selectedMember || this.selectedSelf
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
onLoad: function() {
|
|
onLoad: function() {
|
|
|
- this.loadFamilyMembers()
|
|
|
|
|
- },
|
|
|
|
|
- methods: {
|
|
|
|
|
- selectSelf: function() {
|
|
|
|
|
- this.selectedSelf = true
|
|
|
|
|
- this.selectedMember = null
|
|
|
|
|
- },
|
|
|
|
|
- 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
|
|
|
|
|
- var memberId = this.selectedMember ? this.selectedMember.id : ''
|
|
|
|
|
- uni.uploadFile({
|
|
|
|
|
- url: getApp().globalData.baseUrl + '/api/health/report/parse-preview?type=tongue' + (memberId ? '&memberId=' + memberId : ''),
|
|
|
|
|
- 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 }
|
|
|
|
|
- })
|
|
|
|
|
- var memberId = this.selectedMember ? this.selectedMember.id : ''
|
|
|
|
|
- uni.request({
|
|
|
|
|
- url: getApp().globalData.baseUrl + '/api/health/report/confirm?type=tongue',
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- data: {
|
|
|
|
|
- recordId: this.tongueResult.recordId,
|
|
|
|
|
- memberId: memberId || undefined,
|
|
|
|
|
- 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) {
|
|
|
|
|
- var memberId = this.selectedMember ? this.selectedMember.id : ''
|
|
|
|
|
- uni.request({
|
|
|
|
|
- url: getApp().globalData.baseUrl + '/api/health/report/discard?type=tongue',
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- data: { recordId: self.tongueResult.recordId, memberId: memberId || undefined }
|
|
|
|
|
- })
|
|
|
|
|
- uni.navigateBack()
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ uni.redirectTo({ url: '/pages/health/report-upload?reportType=tongue' })
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
</script>
|
|
</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; }
|
|
|
|
|
-.self-first { width: 140rpx; height: 160rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #FFF7ED; border-radius: 16rpx; border: 2rpx solid #F97316; margin-bottom: 20rpx; }
|
|
|
|
|
-.self-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-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>
|
|
|