|
|
@@ -69,8 +69,41 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- Step 3: 生成方案 -->
|
|
|
+ <!-- Step 3: 问卷内容确认 + 手签名 -->
|
|
|
<view class="section" v-if="step === 3 && !viewMode">
|
|
|
+ <view class="section-title">📝 确认问卷内容</view>
|
|
|
+ <view class="hint-text">请核对以下问卷信息准确无误后,手写签名确认</view>
|
|
|
+
|
|
|
+ <!-- 问卷内容摘要(按成员) -->
|
|
|
+ <view class="confirm-survey-list">
|
|
|
+ <view v-for="m in selectedMembers" :key="m.memberId" class="confirm-survey-member">
|
|
|
+ <view class="confirm-member-name">{{ m.name }}</view>
|
|
|
+ <view class="confirm-summary-row" v-for="(line, li) in buildSurveySummary(m.memberId)" :key="li">
|
|
|
+ <text class="confirm-label">{{ line.label }}</text>
|
|
|
+ <text class="confirm-value">{{ line.value }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="!hasMemberData(m.memberId)" class="confirm-empty">该成员未填写问卷内容</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 手写签名 -->
|
|
|
+ <view class="signature-title">✍️ 手写签名确认</view>
|
|
|
+ <view class="hint-text">签名即表示您确认上述问卷信息填写准确、真实有效</view>
|
|
|
+ <hand-signature
|
|
|
+ ref="signaturePad"
|
|
|
+ :value="signatureDataUrl"
|
|
|
+ @confirm="onSignatureConfirm"
|
|
|
+ @clear="onSignatureClear"
|
|
|
+ />
|
|
|
+
|
|
|
+ <view class="btn-pair">
|
|
|
+ <button class="btn-confirm full-btn" :disabled="!signatureDataUrl || loading" @click="confirmAndGenerate">{{ loading ? '生成中...' : '确认签名并生成方案' }}</button>
|
|
|
+ <button class="btn-skip" @click="step = 2">返回修改问卷</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- Step 4: 生成方案 -->
|
|
|
+ <view class="section" v-if="step === 4 && !viewMode">
|
|
|
<view class="section-title">📊 方案生成</view>
|
|
|
|
|
|
<!-- 加载中 -->
|
|
|
@@ -129,7 +162,7 @@
|
|
|
</view>
|
|
|
|
|
|
<!-- 最终确认保存 (所有 section 确认后) -->
|
|
|
- <view class="section" v-if="step === 3 && allConfirmed && !editing && !loading && !viewMode">
|
|
|
+ <view class="section" v-if="step === 4 && allConfirmed && !editing && !loading && !viewMode">
|
|
|
<view class="section-title">📋 确认方案</view>
|
|
|
<view class="hint-text">所有板块已确认,点击保存生成最终方案</view>
|
|
|
<view class="final-review">
|
|
|
@@ -182,14 +215,15 @@
|
|
|
|
|
|
<script>
|
|
|
import { generateHealthPlan, regenerateSection, confirmPlan, saveHealthPlan, getHealthPlanDetail } from '../../utils/api'
|
|
|
-import { getHealthStatus, saveHealthStatus, getDietPreferences, saveDietPreferences, getMyMembership } from '../../utils/api'
|
|
|
+import { getHealthStatus, saveHealthStatus, getDietPreferences, saveDietPreferences, getMyMembership, saveHealthSignature } from '../../utils/api'
|
|
|
import MemberChipPicker from '@/pages/health/components/MemberChipPicker.vue'
|
|
|
import ComprehensiveSurvey from '@/pages/health/components/ComprehensiveSurvey.vue'
|
|
|
+import HandSignature from '@/components/hand-signature/hand-signature.vue'
|
|
|
import fontScaleMixin from '@/mixins/font-scale.js'
|
|
|
|
|
|
export default {
|
|
|
mixins: [fontScaleMixin],
|
|
|
- components: { MemberChipPicker, ComprehensiveSurvey },
|
|
|
+ components: { MemberChipPicker, ComprehensiveSurvey, HandSignature },
|
|
|
data() {
|
|
|
return {
|
|
|
// 步骤控制
|
|
|
@@ -228,7 +262,9 @@ export default {
|
|
|
confirmingTasks: false,
|
|
|
// UI 状态
|
|
|
loading: false,
|
|
|
- taskSubmitted: false
|
|
|
+ taskSubmitted: false,
|
|
|
+ // Step 3 签名确认
|
|
|
+ signatureDataUrl: ''
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
@@ -241,7 +277,7 @@ export default {
|
|
|
needsTaskConfirm: function() {
|
|
|
return !!(this.planDetail && this.planDetail.status === 'draft')
|
|
|
},
|
|
|
- steps: function() { return ['选人', '问卷', '方案'] },
|
|
|
+ steps: function() { return ['选人', '问卷', '签名', '方案'] },
|
|
|
sectionKeys: function() { return ['nutrition', 'diet', 'exercise'] },
|
|
|
sectionTitles: function() { return ['营养补充建议', '饮食建议', '运动计划'] },
|
|
|
sectionIcons: function() { return ['💊', '🍽️', '🏃'] },
|
|
|
@@ -418,7 +454,8 @@ selectedMembers: function() {
|
|
|
this.step = 2
|
|
|
},
|
|
|
skipToGenerate: function() {
|
|
|
- this.step = 3
|
|
|
+ // 跳过问卷(未填写)→ 无内容可确认,直接进入方案生成(Step 4),无需签名
|
|
|
+ this.step = 4
|
|
|
this.generatePlan()
|
|
|
},
|
|
|
|
|
|
@@ -702,18 +739,132 @@ selectedMembers: function() {
|
|
|
}
|
|
|
})
|
|
|
if (saveTasks.length === 0) {
|
|
|
- // 无问卷数据,直接生成方案
|
|
|
+ // 无问卷数据,直接生成方案(无需签名确认)
|
|
|
self.loading = false
|
|
|
+ self.step = 4
|
|
|
self.generatePlan()
|
|
|
return
|
|
|
}
|
|
|
Promise.all(saveTasks).then(function() {
|
|
|
uni.showToast({ title: '信息已保存', icon: 'success' })
|
|
|
self.loading = false
|
|
|
+ // 进入签名确认步骤,确认后再生成方案
|
|
|
+ self.signatureDataUrl = ''
|
|
|
+ self.step = 3
|
|
|
+ }).catch(function() {
|
|
|
+ // 保存失败也进入确认步骤(签名不依赖保存成功)
|
|
|
+ self.loading = false
|
|
|
+ self.signatureDataUrl = ''
|
|
|
+ self.step = 3
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ // ===== Step 3 签名确认 =====
|
|
|
+ // 按成员生成问卷内容摘要(键值对列表)
|
|
|
+ buildSurveySummary: function(memberId) {
|
|
|
+ var s = this.memberFormMap[String(memberId)] || null
|
|
|
+ var d = this.memberDietMap[String(memberId)] || null
|
|
|
+ var lines = []
|
|
|
+ var push = function(label, value) {
|
|
|
+ if (value && String(value).trim() !== '' && value !== false && !(Array.isArray(value) && value.length === 0)) {
|
|
|
+ lines.push({ label: label, value: Array.isArray(value) ? value.join('、') : String(value) })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (s) {
|
|
|
+ var f = s.form || {}
|
|
|
+ push('身高', f.heightCm ? f.heightCm + ' cm' : '')
|
|
|
+ push('体重', f.weightKg ? f.weightKg + ' kg' : '')
|
|
|
+ push('血压', f.bloodPressure)
|
|
|
+ push('空腹血糖', f.bloodGlucose ? f.bloodGlucose + ' mmol/L' : '')
|
|
|
+ push('总胆固醇', s.lipidCholesterol ? s.lipidCholesterol + ' mmol/L' : '')
|
|
|
+ if (s.selectedDiseases && s.selectedDiseases.length > 0) {
|
|
|
+ push('疾病史', s.selectedDiseases.map(function(x) { return x.name }).join('、'))
|
|
|
+ }
|
|
|
+ if (s.noMedication) push('用药', '无')
|
|
|
+ else if (s.medRows && s.medRows.length > 0) {
|
|
|
+ push('在服药物', s.medRows.map(function(x) { return x.name || '' }).filter(function(n) { return n }).join('、'))
|
|
|
+ }
|
|
|
+ var ls = s.lifestyle || {}
|
|
|
+ push('心情', ls.mood)
|
|
|
+ push('睡眠时长', ls.sleepDuration)
|
|
|
+ push('入睡时间', ls.bedtime)
|
|
|
+ push('排便频率', ls.bowelFrequency)
|
|
|
+ push('排便性状', ls.bowelForm)
|
|
|
+ push('抗生素用药史', ls.antibioticHistory)
|
|
|
+ if (ls.allergySymptoms && ls.allergySymptoms.length > 0) push('过敏表现', ls.allergySymptoms.join('、'))
|
|
|
+ push('月经周期', ls.menstrualCycle)
|
|
|
+ push('特殊时期', ls.specialPeriod)
|
|
|
+ push('生长发育', ls.growthDevelopment)
|
|
|
+ push('挑食情况', ls.pickyEating)
|
|
|
+ push('骨质疏松风险', ls.osteoporosisRisk)
|
|
|
+ push('跌倒风险', ls.fallRisk)
|
|
|
+ push('屏幕时间', ls.screenTime)
|
|
|
+ push('情绪状态', ls.teenMood)
|
|
|
+ push('当前状态', ls.currentStatus)
|
|
|
+ push('运动频率', ls.exerciseFrequency)
|
|
|
+ push('运动时长', ls.exerciseDuration)
|
|
|
+ if (f.notes) push('补充说明', f.notes)
|
|
|
+ }
|
|
|
+ if (d) {
|
|
|
+ push('食物过敏', d.allergies)
|
|
|
+ push('绝对忌口', d.absoluteAvoid)
|
|
|
+ push('必吃食物', d.mustEat)
|
|
|
+ push('宗教饮食', d.religiousDiet && d.religiousDiet !== 'none' ? d.religiousDiet : '')
|
|
|
+ if (Number(d.spiceLevel) > 0) push('辣度', d.spiceLevel)
|
|
|
+ push('口味偏好', d.flavorPref)
|
|
|
+ push('菜系偏好', d.cuisinePref)
|
|
|
+ push('烹饪方式', d.cookingMethods)
|
|
|
+ if (d.noPreference) push('饮食偏好', '无特殊偏好')
|
|
|
+ push('蔬果量', d.vegetableFruit)
|
|
|
+ push('乳制品', d.dairy)
|
|
|
+ push('含糖饮料', d.sugaryDrinks)
|
|
|
+ }
|
|
|
+ return lines
|
|
|
+ },
|
|
|
+ // 该成员是否有问卷/饮食数据可供确认
|
|
|
+ hasMemberData: function(memberId) {
|
|
|
+ var s = this.memberFormMap[String(memberId)]
|
|
|
+ var d = this.memberDietMap[String(memberId)]
|
|
|
+ var hasForm = s && this.hasPayload(this.buildHealthPayload(s))
|
|
|
+ var hasDiet = d && (this.hasDietContent(d) || d.noPreference)
|
|
|
+ return !!(hasForm || hasDiet)
|
|
|
+ },
|
|
|
+ onSignatureConfirm: function(dataUrl) {
|
|
|
+ this.signatureDataUrl = dataUrl || ''
|
|
|
+ },
|
|
|
+ onSignatureClear: function() {
|
|
|
+ this.signatureDataUrl = ''
|
|
|
+ },
|
|
|
+ // 确认签名并生成方案:先把签名逐成员落库,再生成
|
|
|
+ confirmAndGenerate: function() {
|
|
|
+ var self = this
|
|
|
+ if (!self.signatureDataUrl) {
|
|
|
+ uni.showToast({ title: '请先完成手写签名', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (self.loading) return
|
|
|
+ self.loading = true
|
|
|
+ // 为每个有问卷数据的成员落库签名(MEDIUMTEXT 只更新签名列)
|
|
|
+ var sigTasks = []
|
|
|
+ self.selectedMemberIds.forEach(function(mid) {
|
|
|
+ if (self.hasMemberData(mid)) {
|
|
|
+ sigTasks.push(saveHealthSignature(mid, self.signatureDataUrl))
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (sigTasks.length === 0) {
|
|
|
+ self.loading = false
|
|
|
+ self.step = 4
|
|
|
+ self.generatePlan()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ Promise.all(sigTasks).then(function() {
|
|
|
+ self.loading = false
|
|
|
+ self.step = 4
|
|
|
self.generatePlan()
|
|
|
}).catch(function() {
|
|
|
- // 保存失败也继续生成
|
|
|
+ // 签名落库失败不阻塞生成
|
|
|
self.loading = false
|
|
|
+ self.step = 4
|
|
|
self.generatePlan()
|
|
|
})
|
|
|
},
|
|
|
@@ -761,11 +912,11 @@ selectedMembers: function() {
|
|
|
})
|
|
|
self.planData = { overview: commonPart.overview || ((commonSections.length || Object.keys(individualPart).length) ? '' : (data.planContent || '')) }
|
|
|
self.multiMemberPlanJson = { common: data.common, individual: data.individual }
|
|
|
- self.step = 3
|
|
|
+ self.step = 4
|
|
|
} else if (typeof data === 'string') {
|
|
|
self.planData = { overview: data }
|
|
|
self.multiMemberPlanJson = null
|
|
|
- self.step = 3
|
|
|
+ self.step = 4
|
|
|
} else if (data.overview) {
|
|
|
self.planData = data
|
|
|
self.multiMemberPlanJson = null
|
|
|
@@ -777,11 +928,11 @@ selectedMembers: function() {
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
- self.step = 3
|
|
|
+ self.step = 4
|
|
|
} else if (data.raw) {
|
|
|
self.planData = { overview: data.raw.substring(0, 200) }
|
|
|
self.multiMemberPlanJson = null
|
|
|
- self.step = 3
|
|
|
+ self.step = 4
|
|
|
} else {
|
|
|
uni.showToast({ title: '生成失败,请重试', icon: 'none' })
|
|
|
}
|
|
|
@@ -1609,6 +1760,51 @@ selectedMembers: function() {
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
.empty-text { font-size: calc(26rpx * var(--font-scale, 1)); color: #999; }
|
|
|
+
|
|
|
+/* Step 3: 问卷内容确认 + 手签名 */
|
|
|
+.confirm-survey-list {
|
|
|
+ margin-top: 16rpx;
|
|
|
+}
|
|
|
+.confirm-survey-member {
|
|
|
+ background: #F5F7FA;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ padding: 20rpx;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+.confirm-member-name {
|
|
|
+ font-size: calc(26rpx * var(--font-scale, 1));
|
|
|
+ font-weight: bold;
|
|
|
+ color: #F97316;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+.confirm-summary-row {
|
|
|
+ display: flex;
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
+ padding: 8rpx 0;
|
|
|
+ border-bottom: 1rpx solid #EDEDED;
|
|
|
+}
|
|
|
+.confirm-summary-row:last-child { border-bottom: none; }
|
|
|
+.confirm-label {
|
|
|
+ flex: 0 0 160rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+.confirm-value {
|
|
|
+ flex: 1;
|
|
|
+ color: #333;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+.confirm-empty {
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
+ color: #bbb;
|
|
|
+ padding: 20rpx 0;
|
|
|
+}
|
|
|
+.signature-title {
|
|
|
+ font-size: calc(28rpx * var(--font-scale, 1));
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+ margin-top: 30rpx;
|
|
|
+ margin-bottom: 10rpx;
|
|
|
+}
|
|
|
.member-load-hint {
|
|
|
font-size: calc(22rpx * var(--font-scale, 1));
|
|
|
color: #F97316;
|