health-plan-summary.vue 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676
  1. <template>
  2. <view class="plan-page">
  3. <!-- 步骤指示器 -->
  4. <view class="steps-bar" v-if="!viewMode">
  5. <view class="step" v-for="(s, i) in steps" :key="i"
  6. :class="{ active: step === i + 1, done: step > i + 1 }">
  7. <text class="step-num">{{ i + 1 }}</text>
  8. <text class="step-label">{{ s }}</text>
  9. </view>
  10. </view>
  11. <!-- Step 1: 选人 + 目标 -->
  12. <view class="section" v-if="step === 1 && !viewMode">
  13. <view class="section-title-row">
  14. <text class="section-title">选择服务对象</text>
  15. <text class="selected-count-badge" v-if="selectedMemberIds.length > 0">{{ selectedMemberIds.length }} 人已选</text>
  16. </view>
  17. <view class="hint-text">选择方案针对的家庭成员</view>
  18. <member-chip-picker
  19. ref="memberPicker"
  20. :selected-ids="selectedMemberIds"
  21. @change="onPickerChange"
  22. @loaded="onPickerLoaded"
  23. @add-member="onAddMember"
  24. />
  25. <view class="section-title" style="margin-top:20rpx">健康目标</view>
  26. <view class="hint-text">描述您希望改善的方向,AI将为您定制方案</view>
  27. <textarea class="goal-textarea" v-model="userGoal" placeholder="例如:孩子最近睡眠不好,希望改善睡眠质量,同时提升免疫力..."
  28. :maxlength="300" auto-height :disabled="loading" @input="onGoalInput"></textarea>
  29. <view class="goal-footer">
  30. <text class="goal-count">{{ goalLen }}/300</text>
  31. <button class="btn-next" :disabled="!canGenerate" @click="goToStep2">{{ loading ? '加载中...' : '下一步' }}</button>
  32. </view>
  33. </view>
  34. <!-- Step 2: 健康问卷 (嵌入式) -->
  35. <view class="section" v-if="step === 2 && !viewMode">
  36. <view class="section-title">📋 健康问卷</view>
  37. <view class="hint-text">填写健康现状,方案将参考这些信息。可跳过,但填写后方案更精准。</view>
  38. <!-- 多成员分别填写:成员切换 tab -->
  39. <view class="member-tabs" v-if="selectedMembers.length > 1">
  40. <view v-for="m in selectedMembers" :key="m.memberId"
  41. class="member-tab" :class="{ active: m.memberId === activeFormMemberId }"
  42. @click="switchMember(m.memberId)">
  43. <text>{{ m.name }}</text>
  44. </view>
  45. </view>
  46. <view class="form-member-hint" v-if="selectedMembers.length > 1">
  47. <text class="form-member-label">正在为「{{ currentMemberName }}」填写</text>
  48. <text class="form-member-save">每个成员独立保存,切换后自动保留</text>
  49. </view>
  50. <!-- 基础体征 -->
  51. <view class="form-section">
  52. <view class="form-title">基础体征</view>
  53. <view class="form-row">
  54. <text class="form-label">身高 (cm)</text>
  55. <input class="form-input" type="digit" v-model="form.heightCm" placeholder="如 170" />
  56. </view>
  57. <view class="form-row">
  58. <text class="form-label">体重 (kg)</text>
  59. <input class="form-input" type="digit" v-model="form.weightKg" placeholder="如 60" />
  60. </view>
  61. <view class="form-row">
  62. <text class="form-label">血压</text>
  63. <input class="form-input" type="text" v-model="form.bloodPressure" placeholder="如 120/80" />
  64. </view>
  65. <view class="form-row">
  66. <text class="form-label">空腹血糖 (mmol/L)</text>
  67. <input class="form-input" type="digit" v-model="form.bloodGlucose" placeholder="如 5.6" />
  68. </view>
  69. </view>
  70. <!-- 血脂 -->
  71. <view class="form-section">
  72. <view class="form-title">血脂</view>
  73. <view class="form-row">
  74. <text class="form-label">总胆固醇 (mmol/L)</text>
  75. <input class="form-input" type="digit" v-model="lipidCholesterol" placeholder="如 4.5" />
  76. </view>
  77. </view>
  78. <!-- 疾病史 -->
  79. <view class="form-section">
  80. <view class="form-title">疾病史</view>
  81. <view v-for="(group, gi) in diseaseGroups" :key="group.key" class="disease-group">
  82. <view class="group-head" @click="toggleGroup(group.key)">
  83. <text class="group-title">{{ group.title }}</text>
  84. <text class="group-arrow">{{ expandedGroups.indexOf(group.key) >= 0 ? '收起' : '展开' }}</text>
  85. </view>
  86. <view v-if="expandedGroups.indexOf(group.key) >= 0" class="tag-wrap">
  87. <view
  88. v-for="(item, ii) in group.items"
  89. :key="item.name"
  90. class="tag-item"
  91. :class="{ 'tag-active': selectedNames.indexOf(item.name) >= 0 }"
  92. @click="toggleDisease(item.name)">
  93. <text>{{ item.name }}</text>
  94. <text v-if="item.risk" class="risk-badge" @click.stop="showRiskTip(item.name)">★</text>
  95. </view>
  96. </view>
  97. </view>
  98. <view class="custom-row">
  99. <input class="custom-input" type="text" v-model="diseaseInput" placeholder="输入其他疾病" />
  100. <text class="add-link" @click="addCustomDisease">添加</text>
  101. </view>
  102. <view v-if="selectedDiseases.length > 0" class="selected-area">
  103. <view class="selected-title">已选疾病({{ selectedDiseases.length }})</view>
  104. <view v-for="(item, si) in selectedDiseases" :key="getSelKey(si)" class="sel-row">
  105. <text class="sel-name">{{ item.name }}</text>
  106. <picker mode="date" fields="month" :value="item.diagnosedAt || currentMonth" :end="monthEnd" @change="onDiagChange(si, $event)">
  107. <view class="sel-diag">{{ item.diagnosedAt ? item.diagnosedAt : '设确诊时间' }}</view>
  108. </picker>
  109. <text class="sel-del" @click="removeSelected(si)">×</text>
  110. </view>
  111. </view>
  112. </view>
  113. <!-- 在服药物/治疗 -->
  114. <view class="form-section">
  115. <view class="form-title">在服药物/治疗</view>
  116. <view v-for="(med, idx) in medRows" :key="idx" class="med-row">
  117. <view class="med-line">
  118. <input class="med-name" type="text" v-model="med.name" placeholder="药物品牌,如 立普妥" />
  119. <text class="med-del" @click="removeMed(idx)">删除</text>
  120. </view>
  121. <view class="med-line">
  122. <picker mode="date" fields="month" :value="med.startDate || currentMonth" :end="monthEnd" @change="onMedStartChange(idx, $event)">
  123. <view class="med-picker">{{ med.startDate ? med.startDate : '选择开始日期' }}</view>
  124. </picker>
  125. <text class="med-forget" @click="onForgetClick(idx)">记不清</text>
  126. </view>
  127. <view class="med-line" v-if="med.durationText">
  128. <text class="med-duration">已服时长:{{ med.durationText }}</text>
  129. </view>
  130. <view class="med-line">
  131. <input class="med-time" type="text" v-model="med.time" placeholder="服药时间,如 早饭后一次" />
  132. </view>
  133. </view>
  134. <text class="add-link" @click="addMed">+ 添加药物/治疗</text>
  135. </view>
  136. <!-- 其他补充 -->
  137. <view class="form-section">
  138. <view class="form-title">其他补充</view>
  139. <textarea class="notes-area" v-model="form.notes" placeholder="其他想补充的健康信息(选填)" :maxlength="500" />
  140. </view>
  141. <!-- 底部按钮 -->
  142. <view class="btn-pair">
  143. <button class="btn-skip" @click="skipToGenerate">跳过</button>
  144. <button class="btn-generate" :disabled="loading" @click="saveAndGenerate">{{ loading ? '生成中...' : '保存并生成' }}</button>
  145. </view>
  146. </view>
  147. <!-- Step 3: 生成方案 -->
  148. <view class="section" v-if="step === 3 && !viewMode">
  149. <view class="section-title">📊 方案生成</view>
  150. <!-- 加载中 -->
  151. <view v-if="loading" class="loading-state">
  152. <text class="loading-icon">⏳</text>
  153. <text class="loading-text">AI 正在为您生成个性化健康方案...</text>
  154. <text class="loading-hint">预计需要 10-30 秒,请耐心等待</text>
  155. </view>
  156. <!-- 方案预览 -->
  157. <view v-else>
  158. <view class="overview-card" v-if="planData.overview">
  159. <text class="overview-text">{{ planData.overview }}</text>
  160. </view>
  161. <view v-else class="empty-state"><text class="empty-text">方案生成失败,请重试</text></view>
  162. <view class="section-title" style="margin-top:20rpx">方案结构</view>
  163. <view class="section-preview-list">
  164. <view v-for="(sec, i) in sectionKeys" :key="i" class="section-preview-item">
  165. <view class="sec-icon">{{ secIcons[i] }}</view>
  166. <view class="sec-info">
  167. <text class="sec-title">{{ secTitles[i] }}</text>
  168. <text class="sec-status" :class="{ done: sections[sec].confirmed }">{{ sections[sec].confirmed ? '已确认' : '待确认' }}</text>
  169. </view>
  170. </view>
  171. </view>
  172. <!-- 两个并排按钮:修改确认 / 选择规划师 -->
  173. <view class="btn-pair">
  174. <button class="btn-confirm" :disabled="loading" @click="startEdit">{{ loading ? '处理中...' : '修改确认' }}</button>
  175. <button class="btn-teacher" @click="goToTeacherService">选择规划师</button>
  176. </view>
  177. <view class="back-link" @click="step = 2">← 返回修改问卷</view>
  178. </view>
  179. </view>
  180. <!-- Step 3 编辑模式:各 section 编辑 -->
  181. <view class="section" v-if="editing && !viewMode">
  182. <view class="section-title-row">
  183. <text class="section-title">{{ currentSectionTitle }}</text>
  184. <text class="section-badge">{{ currentSectionKey }}</text>
  185. </view>
  186. <view class="hint-text">{{ currentSectionHint }}</view>
  187. <textarea class="edit-textarea" v-model="sections[currentSectionKey].content"
  188. :disabled="loading" auto-height></textarea>
  189. <view class="feedback-row">
  190. <input class="feedback-input" v-model="sectionFeedback" placeholder="如有修改意见请填写(可选)..." />
  191. </view>
  192. <view class="section-actions">
  193. <button class="btn-edit" @click="regenerateCurrentSection" :disabled="loading">🔄 重新生成</button>
  194. <button class="btn-confirm" @click="confirmCurrentSection" :disabled="loading">保存</button>
  195. </view>
  196. <view class="back-link" @click="exitEditMode">← 返回方案预览</view>
  197. </view>
  198. <!-- 最终确认保存 (所有 section 确认后) -->
  199. <view class="section" v-if="step === 3 && allConfirmed && !editing && !loading && !viewMode">
  200. <view class="section-title">📋 确认方案</view>
  201. <view class="hint-text">所有板块已确认,点击保存生成最终方案</view>
  202. <view class="final-review">
  203. <view v-for="(sec, i) in sectionKeys" :key="i" class="review-item">
  204. <text class="review-title">{{ secTitles[i] }}</text>
  205. <text class="review-preview">{{ sections[sec].content.substring(0, 80) }}{{ sections[sec].content.length > 80 ? '...' : '' }}</text>
  206. </view>
  207. </view>
  208. <view class="btn-pair">
  209. <button class="btn-confirm full-btn" :disabled="loading" @click="submitPlan">{{ loading ? '保存中...' : '确认保存' }}</button>
  210. <button class="btn-teacher" @click="goToTeacherService">选择规划师</button>
  211. </view>
  212. <view class="back-link" @click="startEdit">← 继续修改</view>
  213. </view>
  214. <!-- 查看模式 -->
  215. <view class="section plan-view-section" v-if="viewMode && planDetail">
  216. <view class="section-header">
  217. <text class="section-title">📊 健康方案</text>
  218. </view>
  219. <view class="plan-meta">
  220. <text class="plan-meta-item">目标:{{ planDetail.goal || '—' }}</text>
  221. <text class="plan-meta-item" v-if="planDetail.memberName">成员:{{ planDetail.memberName }}</text>
  222. <text class="plan-meta-item" v-if="planDetail.createdAt">制定于 {{ formatTime(planDetail.createdAt) }}</text>
  223. <text class="plan-meta-item" v-if="planDetail.status === 'confirmed' || planDetail.status === 'published'">状态:已发布</text>
  224. <text class="plan-meta-item" v-if="planDetail.status === 'draft'">状态:待确认</text>
  225. <text class="plan-meta-item" v-if="planDetail.status === 'pending_review'">状态:待规划师审核</text>
  226. <text class="plan-meta-item" v-if="planDetail.status === 'rejected'">状态:已驳回</text>
  227. <text class="plan-meta-item" v-if="planDetail.reviewComment && planDetail.status !== 'draft'">审核备注:{{ planDetail.reviewComment }}</text>
  228. </view>
  229. <view v-if="planDetail.planContent" class="plan-preview">
  230. <text class="plan-preview-text">{{ planDetail.planContent }}</text>
  231. </view>
  232. <!-- 待确认:点「确定」分解任务(任务在首页/任务中心查看,不在方案下方展示);「申请规划师」预留 -->
  233. <view class="btn-pair plan-confirm-actions" v-if="needsTaskConfirm">
  234. <button class="btn-confirm" :disabled="confirmingTasks" @click="confirmPlanTasks">{{ confirmingTasks ? '处理中...' : '确定' }}</button>
  235. <button class="btn-teacher" @click="applyForTeacher">申请规划师</button>
  236. </view>
  237. </view>
  238. <!-- 成功遮罩 -->
  239. <view v-if="taskSubmitted" class="success-overlay">
  240. <view class="success-card">
  241. <text class="success-icon">✓</text>
  242. <text class="success-title">{{ planDetail && (planDetail.status === 'published' || planDetail.status === 'confirmed') ? '方案已发布' : '方案已提交审核' }}</text>
  243. <text class="success-sub">已为您创建日常任务</text>
  244. <button class="btn-done" @click="goBack">完成</button>
  245. </view>
  246. </view>
  247. </view>
  248. </template>
  249. <script>
  250. import { generateHealthPlan, regenerateSection, confirmPlan, saveHealthPlan, getHealthPlanDetail, approvePlan } from '../../utils/api'
  251. import { getHealthStatus, saveHealthStatus } from '../../utils/api'
  252. import MemberChipPicker from '../../components/MemberChipPicker.vue'
  253. // v2: 预置疾病清单(菌群报告病症风险 ★ + 互联网统计常见病 两源合并去重,8 组 54 项)
  254. var DISEASE_GROUPS = [
  255. { key: 'cardio', title: '心血管代谢', items: [
  256. { name: '高血压', risk: false },
  257. { name: '糖尿病', risk: false },
  258. { name: '血脂异常', risk: false },
  259. { name: '冠心病', risk: false },
  260. { name: '脑卒中', risk: false },
  261. { name: '心力衰竭', risk: false },
  262. { name: '心房颤动', risk: false },
  263. { name: '慢阻肺', risk: false },
  264. { name: '心脑血管疾病', risk: true },
  265. { name: 'II型糖尿病', risk: true },
  266. { name: '肥胖', risk: true },
  267. { name: '超重', risk: false },
  268. { name: '高尿酸血症', risk: false },
  269. { name: '骨质疏松', risk: false }
  270. ]},
  271. { key: 'digest', title: '消化系统', items: [
  272. { name: '幽门螺杆菌感染', risk: false },
  273. { name: '脂肪肝', risk: false },
  274. { name: '慢性胃炎', risk: false },
  275. { name: '胃食管反流', risk: false },
  276. { name: '肠易激综合征', risk: true },
  277. { name: '炎症性肠炎', risk: true },
  278. { name: '慢性便秘', risk: true },
  279. { name: '感染性腹泻', risk: true },
  280. { name: '胆结石', risk: false },
  281. { name: '炎症性肠病', risk: false },
  282. { name: '肝病', risk: true },
  283. { name: '胃病', risk: true },
  284. { name: '胆病', risk: true },
  285. { name: '肾病', risk: true },
  286. { name: '腹胀', risk: true }
  287. ]},
  288. { key: 'immune', title: '过敏免疫', items: [
  289. { name: '过敏性鼻炎', risk: true },
  290. { name: '哮喘', risk: false },
  291. { name: '湿疹', risk: false },
  292. { name: '食物过敏', risk: false },
  293. { name: '自体免疫病', risk: true }
  294. ]},
  295. { key: 'neuro', title: '精神神经', items: [
  296. { name: '抑郁症', risk: true },
  297. { name: '失眠', risk: true },
  298. { name: '焦虑障碍', risk: false },
  299. { name: '偏头痛', risk: false },
  300. { name: '自闭症', risk: true }
  301. ]},
  302. { key: 'resp', title: '呼吸系统', items: [
  303. { name: '肺部感染或疾病', risk: true },
  304. { name: '肺部疾病', risk: true }
  305. ]},
  306. { key: 'tumor', title: '肿瘤', items: [
  307. { name: '结直肠癌', risk: true },
  308. { name: '肺癌', risk: false },
  309. { name: '甲状腺癌', risk: false },
  310. { name: '肝癌', risk: false },
  311. { name: '胃癌', risk: false },
  312. { name: '乳腺癌', risk: false },
  313. { name: '食管癌', risk: false }
  314. ]},
  315. { key: 'child', title: '儿童青少年', items: [
  316. { name: '近视', risk: false },
  317. { name: '龋齿', risk: false },
  318. { name: '注意缺陷多动障碍', risk: false },
  319. { name: '甲状腺疾病', risk: true }
  320. ]},
  321. { key: 'other', title: '其他', items: [
  322. { name: '缺铁性贫血', risk: false },
  323. { name: '贫血', risk: false }
  324. ]}
  325. ]
  326. export default {
  327. components: { MemberChipPicker },
  328. data() {
  329. return {
  330. // 步骤控制
  331. step: 1,
  332. // 成员选择(字段名对齐 report-list,避免 executors 在部分基础库 setData 异常)
  333. selectedMemberIds: [],
  334. pickerMembers: [],
  335. needReloadMembers: false,
  336. // 目标
  337. userGoal: '',
  338. goalLen: 0,
  339. // 健康问卷表单
  340. form: {
  341. heightCm: '',
  342. weightKg: '',
  343. bloodPressure: '',
  344. bloodGlucose: '',
  345. notes: ''
  346. },
  347. lipidCholesterol: '',
  348. // 疾病清单很大,不放进首屏 data,避免微信 setData 超限导致成员列表无法渲染
  349. expandedGroups: [],
  350. selectedDiseases: [],
  351. diseaseInput: '',
  352. medRows: [{ name: '', startDate: '', durationText: '', time: '' }],
  353. currentMonth: '',
  354. monthEnd: '',
  355. // 多成员分别填写:memberId → 各自的表单快照 { form, lipidCholesterol, selectedDiseases, medRows }
  356. memberFormMap: {},
  357. activeFormMemberId: '',
  358. // 方案数据
  359. planData: {},
  360. sections: {
  361. nutrition: { content: '', confirmed: false },
  362. diet: { content: '', confirmed: false },
  363. exercise: { content: '', confirmed: false }
  364. },
  365. sectionFeedback: '',
  366. editing: false,
  367. // 查看模式
  368. viewMode: false,
  369. planDetail: null,
  370. planId: null,
  371. confirmingTasks: false,
  372. // UI 状态
  373. loading: false,
  374. taskSubmitted: false
  375. }
  376. },
  377. computed: {
  378. // 仅在问卷步骤暴露疾病清单,减小 step1 的 setData 体积
  379. diseaseGroups: function() {
  380. return this.step >= 2 ? DISEASE_GROUPS : []
  381. },
  382. canGenerate: function() {
  383. return this.selectedMemberIds.length > 0 && this.goalLen > 0 && !this.loading
  384. },
  385. allConfirmed: function() {
  386. return this.sections.nutrition.confirmed && this.sections.diet.confirmed && this.sections.exercise.confirmed
  387. },
  388. needsTaskConfirm: function() {
  389. return !!(this.planDetail && this.planDetail.status === 'draft')
  390. },
  391. steps: function() { return ['选人', '问卷', '方案'] },
  392. sectionKeys: function() { return ['nutrition', 'diet', 'exercise'] },
  393. sectionTitles: function() { return ['营养补充建议', '饮食建议', '运动计划'] },
  394. sectionIcons: function() { return ['💊', '🍽️', '🏃'] },
  395. sectionHints: function() { return [
  396. '包含益生菌、维生素、矿物质等营养补充建议',
  397. '包含早餐、午餐、晚餐的饮食建议',
  398. '包含有氧运动、力量训练等运动计划'
  399. ]},
  400. currentSectionKey: function() {
  401. var idx = this.step - 3
  402. return this.sectionKeys[idx] || ''
  403. },
  404. currentSectionTitle: function() {
  405. var idx = this.step - 3
  406. return this.sectionTitles[idx] || ''
  407. },
  408. currentSectionHint: function() {
  409. var idx = this.step - 3
  410. return this.sectionHints[idx] || ''
  411. },
  412. selectedNames: function() {
  413. return this.selectedDiseases.map(function(d) { return d.name })
  414. },
  415. selectedMembers: function() {
  416. var self = this
  417. var out = []
  418. this.selectedMemberIds.forEach(function(id) {
  419. var found = null
  420. for (var i = 0; i < self.pickerMembers.length; i++) {
  421. var mid = self.pickerMembers[i].memberId || self.pickerMembers[i].id
  422. if (String(mid) === String(id)) { found = self.pickerMembers[i]; break }
  423. }
  424. if (found) out.push(found)
  425. })
  426. return out
  427. },
  428. currentMemberName: function() {
  429. var members = this.selectedMembers
  430. for (var i = 0; i < members.length; i++) {
  431. if (String(members[i].memberId || members[i].id) === String(this.activeFormMemberId)) {
  432. return members[i].name || members[i].nickname || '成员'
  433. }
  434. }
  435. return '成员'
  436. }
  437. },
  438. onLoad: function(options) {
  439. var self = this
  440. // 处理初始成员ID
  441. if (options.initialMemberIds) {
  442. this.selectedMemberIds = options.initialMemberIds.split(',').filter(function(id) { return id }).map(function(id) { return String(parseInt(id) || id) })
  443. } else if (options.memberId) {
  444. this.selectedMemberIds = [String(options.memberId)]
  445. }
  446. // 处理 planId 查看模式
  447. this.planId = options.planId ? parseInt(options.planId) : null
  448. if (this.planId) {
  449. this.viewMode = true
  450. this.loadPlanDetail()
  451. }
  452. // 初始化健康问卷时间
  453. var now = new Date()
  454. var y = now.getFullYear()
  455. var m = now.getMonth() + 1
  456. var mStr = m < 10 ? '0' + m : '' + m
  457. this.currentMonth = y + '-' + mStr
  458. var lastDay = new Date(y, m, 0).getDate()
  459. this.monthEnd = y + '-' + mStr + '-' + (lastDay < 10 ? '0' + lastDay : '' + lastDay)
  460. // 健康问卷数据在成员选择组件 loaded 后按成员异步加载
  461. // 自动执行
  462. if (options.autoExecute === '1') {
  463. setTimeout(function() { self.doAutoExecute() }, 1500)
  464. }
  465. },
  466. onShow: function() {
  467. if (this.needReloadMembers) {
  468. this.needReloadMembers = false
  469. if (this.$refs.memberPicker && this.$refs.memberPicker.reload) {
  470. this.$refs.memberPicker.reload()
  471. }
  472. }
  473. },
  474. methods: {
  475. onGoalInput: function(e) {
  476. var val = (e && e.detail && e.detail.value) || this.userGoal || ''
  477. this.goalLen = String(val).length
  478. },
  479. onPickerChange: function(payload) {
  480. if (!payload) return
  481. this.selectedMemberIds = payload.selectedIds || []
  482. this.pickerMembers = payload.members || []
  483. },
  484. onPickerLoaded: function(payload) {
  485. if (!payload) return
  486. this.selectedMemberIds = payload.selectedIds || []
  487. this.pickerMembers = payload.members || []
  488. var formId = (payload.selectedIds && payload.selectedIds[0]) || ''
  489. if (formId) {
  490. this.activeFormMemberId = String(formId)
  491. var self = this
  492. setTimeout(function() { self.loadHealthForm(formId) }, 100)
  493. }
  494. },
  495. onAddMember: function() {
  496. this.needReloadMembers = true
  497. },
  498. // ===== 时间格式化 =====
  499. formatTime: function(iso) {
  500. if (!iso) return ''
  501. var s = String(iso)
  502. if (s.length >= 19) s = s.substring(0, 19)
  503. return s.replace('T', ' ')
  504. },
  505. // ===== 步骤导航 =====
  506. goToStep2: function() {
  507. if (this.expandedGroups.length === 0) {
  508. this.expandedGroups = DISEASE_GROUPS.slice(0, 3).map(function(g) { return g.key })
  509. }
  510. this.step = 2
  511. },
  512. skipToGenerate: function() {
  513. this.step = 3
  514. this.generatePlan()
  515. },
  516. // ===== 健康问卷方法 (从 health-status-form.vue 移植,支持按成员加载) =====
  517. loadHealthForm: function(memberId) {
  518. var self = this
  519. var mid = memberId || self.activeFormMemberId || ''
  520. // 已有该成员的本地快照 → 直接恢复
  521. if (mid && self.memberFormMap[mid]) {
  522. self.applyFormSnapshot(self.memberFormMap[mid])
  523. return
  524. }
  525. getHealthStatus(mid || undefined).then(function(res) {
  526. if (res && res.code === 200 && res.data) {
  527. var d = res.data
  528. var snapshot = {
  529. form: {
  530. heightCm: d.heightCm || '',
  531. weightKg: d.weightKg || '',
  532. bloodPressure: d.bloodPressure || '',
  533. bloodGlucose: d.bloodGlucose || '',
  534. notes: d.notes || ''
  535. },
  536. lipidCholesterol: self.parseLipidCholesterol(d.bloodLipids),
  537. selectedDiseases: [],
  538. medRows: self.parseMeds(d.medications)
  539. }
  540. self.memberFormMap[mid || '__account'] = snapshot
  541. self.loadDiseasesInto(snapshot.selectedDiseases, d.diseaseHistory)
  542. if (mid) self.applyFormSnapshot(snapshot)
  543. else self.applyFormSnapshot({
  544. form: snapshot.form,
  545. lipidCholesterol: snapshot.lipidCholesterol,
  546. selectedDiseases: snapshot.selectedDiseases,
  547. medRows: snapshot.medRows
  548. })
  549. }
  550. }).catch(function() {})
  551. },
  552. // 切换成员时先保存当前编辑内容,再加载目标成员快照
  553. switchMember: function(memberId) {
  554. var self = this
  555. if (String(memberId) === String(self.activeFormMemberId)) return
  556. // 保存当前成员到快照
  557. self.memberFormMap[self.activeFormMemberId || '__account'] = {
  558. form: JSON.parse(JSON.stringify(self.form)),
  559. lipidCholesterol: self.lipidCholesterol,
  560. selectedDiseases: JSON.parse(JSON.stringify(self.selectedDiseases)),
  561. medRows: JSON.parse(JSON.stringify(self.medRows))
  562. }
  563. self.activeFormMemberId = String(memberId)
  564. // 目标成员已有快照 → 恢复;无则按该成员从服务端加载
  565. if (self.memberFormMap[self.activeFormMemberId]) {
  566. self.applyFormSnapshot(self.memberFormMap[self.activeFormMemberId])
  567. } else {
  568. self.resetFormFields()
  569. self.loadHealthForm(self.activeFormMemberId)
  570. }
  571. },
  572. applyFormSnapshot: function(snapshot) {
  573. if (!snapshot) return
  574. this.form = JSON.parse(JSON.stringify(snapshot.form || {}))
  575. this.lipidCholesterol = snapshot.lipidCholesterol || ''
  576. this.selectedDiseases = JSON.parse(JSON.stringify(snapshot.selectedDiseases || []))
  577. this.medRows = snapshot.medRows && snapshot.medRows.length > 0 ? JSON.parse(JSON.stringify(snapshot.medRows)) : [{ name: '', startDate: '', durationText: '', time: '' }]
  578. var self = this
  579. this.selectedDiseases.forEach(function(item) {
  580. var gk = self.findGroupKey(item.name)
  581. if (gk && self.expandedGroups.indexOf(gk) < 0) self.expandedGroups.push(gk)
  582. })
  583. },
  584. resetFormFields: function() {
  585. this.form = { heightCm: '', weightKg: '', bloodPressure: '', bloodGlucose: '', notes: '' }
  586. this.lipidCholesterol = ''
  587. this.selectedDiseases = []
  588. this.medRows = [{ name: '', startDate: '', durationText: '', time: '' }]
  589. this.diseaseInput = ''
  590. },
  591. parseLipidCholesterol: function(json) {
  592. try {
  593. var arr = JSON.parse(json || '[]')
  594. if (arr instanceof Array && arr.length > 0 && arr[0].value) {
  595. return String(arr[0].value)
  596. }
  597. } catch (e) {}
  598. return ''
  599. },
  600. loadDiseasesInto: function(targetList, json) {
  601. var self = this
  602. try {
  603. var arr = JSON.parse(json || '[]')
  604. if (arr instanceof Array) {
  605. arr.forEach(function(item) {
  606. if (item.name && self.selectedNames.indexOf(item.name) < 0) {
  607. targetList.push({
  608. name: item.name,
  609. diagnosedAt: item.diagnosedAt || '',
  610. custom: false
  611. })
  612. var gk = self.findGroupKey(item.name)
  613. if (gk && self.expandedGroups.indexOf(gk) < 0) {
  614. self.expandedGroups.push(gk)
  615. }
  616. }
  617. })
  618. }
  619. } catch (e) {}
  620. },
  621. parseMeds: function(json) {
  622. var list = []
  623. try {
  624. var arr = JSON.parse(json || '[]')
  625. if (arr instanceof Array) {
  626. arr.forEach(function(item) {
  627. list.push({
  628. name: item.name || '',
  629. startDate: item.startDate || '',
  630. durationText: item.durationText || '',
  631. time: item.time || ''
  632. })
  633. })
  634. }
  635. } catch (e) {}
  636. if (list.length === 0) list.push({ name: '', startDate: '', durationText: '', time: '' })
  637. return list
  638. },
  639. toggleGroup: function(key) {
  640. var idx = this.expandedGroups.indexOf(key)
  641. if (idx >= 0) this.expandedGroups.splice(idx, 1)
  642. else this.expandedGroups.push(key)
  643. },
  644. findGroupKey: function(name) {
  645. for (var i = 0; i < DISEASE_GROUPS.length; i++) {
  646. var items = DISEASE_GROUPS[i].items
  647. for (var j = 0; j < items.length; j++) {
  648. if (items[j].name === name) return DISEASE_GROUPS[i].key
  649. }
  650. }
  651. return ''
  652. },
  653. toggleDisease: function(name) {
  654. var idx = -1
  655. for (var i = 0; i < this.selectedDiseases.length; i++) {
  656. if (this.selectedDiseases[i].name === name) { idx = i; break }
  657. }
  658. if (idx >= 0) {
  659. this.selectedDiseases.splice(idx, 1)
  660. } else {
  661. this.selectedDiseases.push({ name: name, diagnosedAt: '', custom: false })
  662. var gk = this.findGroupKey(name)
  663. if (gk && this.expandedGroups.indexOf(gk) < 0) {
  664. this.expandedGroups.push(gk)
  665. }
  666. }
  667. },
  668. showRiskTip: function(name) {
  669. uni.showModal({
  670. title: '菌群报告关联',
  671. content: '「' + name + '」与肠道菌群报告相关,勾选后出方案时可结合菌群报告风险值评估。',
  672. showCancel: false
  673. })
  674. },
  675. addCustomDisease: function() {
  676. var name = (this.diseaseInput || '').trim()
  677. if (!name) return
  678. if (this.selectedNames.indexOf(name) < 0) {
  679. this.selectedDiseases.push({ name: name, diagnosedAt: '', custom: true })
  680. }
  681. this.diseaseInput = ''
  682. },
  683. getSelKey: function(i) { return 's' + i },
  684. removeSelected: function(idx) {
  685. this.selectedDiseases.splice(idx, 1)
  686. },
  687. onDiagChange: function(idx, e) {
  688. this.selectedDiseases[idx].diagnosedAt = e.detail.value
  689. },
  690. addMed: function() {
  691. this.medRows.push({ name: '', startDate: '', durationText: '', time: '' })
  692. },
  693. onMedStartChange: function(idx, e) {
  694. var start = e.detail.value
  695. this.medRows[idx].startDate = start
  696. this.medRows[idx].durationText = this.calcDuration(start)
  697. },
  698. onForgetClick: function(idx) {
  699. var self = this
  700. uni.showModal({
  701. title: '记不清开始日期',
  702. editable: true,
  703. placeholderText: '如:约 3 个月',
  704. success: function(res) {
  705. if (res.confirm && res.content) {
  706. self.medRows[idx].durationText = res.content.trim()
  707. }
  708. }
  709. })
  710. },
  711. calcDuration: function(startDate) {
  712. if (!startDate) return ''
  713. var parts = startDate.split('-')
  714. if (parts.length !== 2) return ''
  715. var sy = parseInt(parts[0], 10)
  716. var sm = parseInt(parts[1], 10)
  717. var now = new Date()
  718. var months = (now.getFullYear() - sy) * 12 + (now.getMonth() + 1 - sm)
  719. if (months < 1) months = 1
  720. if (months < 12) return '已服 ' + months + ' 个月'
  721. return '已服 ' + Math.floor(months / 12) + ' 年'
  722. },
  723. removeMed: function(idx) {
  724. if (this.medRows.length > 1) this.medRows.splice(idx, 1)
  725. },
  726. buildHealthPayload: function(snapshot) {
  727. var s = snapshot || {
  728. form: this.form,
  729. lipidCholesterol: this.lipidCholesterol,
  730. selectedDiseases: this.selectedDiseases,
  731. medRows: this.medRows
  732. }
  733. var lipids = []
  734. if (s.lipidCholesterol) {
  735. lipids.push({ name: '总胆固醇', value: parseFloat(s.lipidCholesterol), unit: 'mmol/L', note: '' })
  736. }
  737. var diseases = []
  738. ;(s.selectedDiseases || []).forEach(function(item) {
  739. var d = { name: item.name }
  740. if (item.diagnosedAt) d.diagnosedAt = item.diagnosedAt
  741. diseases.push(d)
  742. })
  743. var meds = []
  744. ;(s.medRows || []).forEach(function(item) {
  745. if (item.name) {
  746. var m = { name: item.name }
  747. if (item.startDate) m.startDate = item.startDate
  748. if (item.durationText) m.durationText = item.durationText
  749. if (item.time) m.time = item.time
  750. meds.push(m)
  751. }
  752. })
  753. var payload = {}
  754. if (s.form.heightCm) payload.heightCm = Number(s.form.heightCm)
  755. if (s.form.weightKg) payload.weightKg = Number(s.form.weightKg)
  756. if (s.form.bloodPressure) payload.bloodPressure = s.form.bloodPressure
  757. if (s.form.bloodGlucose) payload.bloodGlucose = Number(s.form.bloodGlucose)
  758. if (lipids.length > 0) payload.bloodLipids = JSON.stringify(lipids)
  759. if (diseases.length > 0) payload.diseaseHistory = JSON.stringify(diseases)
  760. if (meds.length > 0) payload.medications = JSON.stringify(meds)
  761. if (s.form.notes) payload.notes = s.form.notes
  762. return payload
  763. },
  764. // 当前成员快照(无改动数据时返回 null)
  765. snapshotForMember: function(memberId) {
  766. var key = String(memberId)
  767. // 优先用已保存的成员快照,其次当前活动表单
  768. if (this.memberFormMap[key]) {
  769. var s = this.memberFormMap[key]
  770. var p = this.buildHealthPayload(s)
  771. return this.hasPayload(p) ? p : null
  772. }
  773. // 活动成员直接用当前表单
  774. if (key === String(this.activeFormMemberId)) {
  775. var activePayload = this.buildHealthPayload()
  776. return this.hasPayload(activePayload) ? activePayload : null
  777. }
  778. return null
  779. },
  780. hasPayload: function(payload) {
  781. if (!payload) return false
  782. return payload.heightCm || payload.weightKg || payload.bloodPressure || payload.bloodGlucose ||
  783. payload.bloodLipids || payload.diseaseHistory || payload.medications || payload.notes
  784. },
  785. // 先保存当前活动成员的编辑内容到快照,再逐个成员保存健康档案,最后生成方案
  786. saveAndGenerate: function() {
  787. var self = this
  788. // 保存当前活动成员到快照
  789. if (self.activeFormMemberId) {
  790. self.memberFormMap[self.activeFormMemberId] = {
  791. form: JSON.parse(JSON.stringify(self.form)),
  792. lipidCholesterol: self.lipidCholesterol,
  793. selectedDiseases: JSON.parse(JSON.stringify(self.selectedDiseases)),
  794. medRows: JSON.parse(JSON.stringify(self.medRows))
  795. }
  796. }
  797. // 血压格式校验(检查所有成员)
  798. var members = self.selectedMemberIds.slice()
  799. for (var i = 0; i < members.length; i++) {
  800. var payload = self.snapshotForMember(members[i])
  801. if (payload && payload.bloodPressure && !/^\d{2,3}\/\d{2,3}$/.test(payload.bloodPressure)) {
  802. uni.showToast({ title: '血压格式应为 120/80', icon: 'none' })
  803. return
  804. }
  805. }
  806. self.loading = true
  807. var saveTasks = []
  808. members.forEach(function(mid) {
  809. var payload = self.snapshotForMember(mid)
  810. if (!payload) return
  811. payload.memberId = mid
  812. saveTasks.push(saveHealthStatus(payload))
  813. })
  814. if (saveTasks.length === 0) {
  815. // 无健康数据,直接生成方案
  816. self.loading = false
  817. self.generatePlan()
  818. return
  819. }
  820. Promise.all(saveTasks).then(function() {
  821. uni.showToast({ title: '健康信息已保存', icon: 'success' })
  822. self.loading = false
  823. self.generatePlan()
  824. }).catch(function() {
  825. // 保存失败也继续生成
  826. self.loading = false
  827. self.generatePlan()
  828. })
  829. },
  830. // ===== 方案生成与编辑 (保持原有逻辑) =====
  831. generatePlan: function() {
  832. var self = this
  833. if (!self.canGenerate) return
  834. self.loading = true
  835. var params = {
  836. memberIds: self.selectedMemberIds.join(','),
  837. dimensions: 'body',
  838. goal: self.userGoal.trim()
  839. }
  840. generateHealthPlan(params).then(function(res) {
  841. self.loading = false
  842. if (res && res.code === 200 && res.data) {
  843. var data = res.data
  844. if (typeof data === 'string') {
  845. self.planData = { overview: data }
  846. self.step = 3
  847. } else if (data.overview) {
  848. self.planData = data
  849. if (data.sections && data.sections instanceof Array) {
  850. data.sections.forEach(function(sec) {
  851. if (sec.key && self.sections[sec.key]) {
  852. self.sections[sec.key].content = sec.content || ''
  853. self.sections[sec.key].items = sec.items || []
  854. }
  855. })
  856. }
  857. self.step = 3
  858. } else if (data.raw) {
  859. self.planData = { overview: data.raw.substring(0, 200) }
  860. self.step = 3
  861. } else {
  862. uni.showToast({ title: '生成失败,请重试', icon: 'none' })
  863. }
  864. } else {
  865. uni.showToast({ title: res && res.message ? res.message : '生成失败', icon: 'none' })
  866. }
  867. }).catch(function() {
  868. self.loading = false
  869. uni.showToast({ title: '请求失败,请检查网络', icon: 'none' })
  870. })
  871. },
  872. doAutoExecute: function() {
  873. if (this.selectedMemberIds.length === 0) return
  874. if (!this.userGoal) this.userGoal = '基于菌群检测报告的个性化健康管理方案'
  875. this.generatePlan()
  876. },
  877. regenerateCurrentSection: function() {
  878. var self = this
  879. var key = self.currentSectionKey
  880. var existing = self.sections[key].content
  881. if (!existing) return
  882. self.loading = true
  883. var params = {
  884. section: key,
  885. feedback: self.sectionFeedback || '',
  886. existing_section_content: existing,
  887. memberIds: self.selectedMemberIds.join(','),
  888. dimensions: 'body',
  889. goal: self.userGoal.trim()
  890. }
  891. regenerateSection(params).then(function(res) {
  892. self.loading = false
  893. if (res && res.code === 200 && res.data) {
  894. self.sections[key].content = res.data.content || res.data
  895. if (res.data.tasks) {
  896. self.sections[key].tasks = res.data.tasks
  897. }
  898. uni.showToast({ title: '生成成功', icon: 'success' })
  899. } else {
  900. uni.showToast({ title: res && res.message ? res.message : '生成失败', icon: 'none' })
  901. }
  902. }).catch(function() {
  903. self.loading = false
  904. uni.showToast({ title: '请求失败', icon: 'none' })
  905. })
  906. },
  907. confirmCurrentSection: function() {
  908. var self = this
  909. self.sections[self.currentSectionKey].confirmed = true
  910. self.editing = false
  911. self.sectionFeedback = ''
  912. // 检查是否全部确认
  913. if (self.allConfirmed) {
  914. // 保持在 step 3,显示最终确认区域
  915. }
  916. },
  917. startEdit: function() {
  918. this.editing = true
  919. this.sectionFeedback = ''
  920. },
  921. exitEditMode: function() {
  922. this.editing = false
  923. this.sectionFeedback = ''
  924. },
  925. submitPlan: function() {
  926. var self = this
  927. self.loading = true
  928. var planJson = JSON.stringify({
  929. overview: self.planData.overview || '',
  930. sections: self.sectionKeys.map(function(key) {
  931. return { key: key, content: self.sections[key].content, items: self.sections[key].items || [], tasks: self.sections[key].tasks || [] }
  932. })
  933. })
  934. var saveData = {
  935. memberIds: self.selectedMemberIds.join(','),
  936. dimensions: 'body',
  937. goal: self.userGoal.trim(),
  938. planContent: self.buildPlanContent(),
  939. planJson: planJson,
  940. status: 'published'
  941. }
  942. saveHealthPlan(saveData).then(function(res) {
  943. self.loading = false
  944. if (res && res.code === 200) {
  945. uni.showToast({ title: '方案已生成', icon: 'none', duration: 2000 })
  946. setTimeout(function() { uni.navigateBack() }, 1200)
  947. self.taskSubmitted = true
  948. } else {
  949. uni.showToast({ title: res && res.message ? res.message : '保存失败', icon: 'none' })
  950. }
  951. }).catch(function() {
  952. self.loading = false
  953. uni.showToast({ title: '请求失败', icon: 'none' })
  954. })
  955. },
  956. buildPlanContent: function() {
  957. var sb = []
  958. if (this.planData.overview) {
  959. sb.push('## 方案概述\n\n' + this.planData.overview + '\n\n')
  960. }
  961. var titles = { nutrition: '营养补充建议', diet: '饮食建议', exercise: '运动计划' }
  962. var self = this
  963. this.sectionKeys.forEach(function(key) {
  964. if (self.sections[key].content) {
  965. sb.push('## ' + (titles[key] || key) + '\n\n' + self.sections[key].content + '\n\n')
  966. }
  967. })
  968. return sb.join('')
  969. },
  970. goToTeacherService: function() {
  971. uni.navigateTo({ url: '/pages/health/teacher-service' })
  972. },
  973. // ===== 查看模式 (保持原有) =====
  974. loadPlanDetail: function() {
  975. var self = this
  976. getHealthPlanDetail({ planId: self.planId }).then(function(res) {
  977. if (res && res.code === 200 && res.data) {
  978. self.planDetail = res.data
  979. self.userGoal = res.data.goal != null ? String(res.data.goal) : ''
  980. if (res.data.memberIds) {
  981. self.selectedMemberIds = res.data.memberIds.split(',').filter(function(id) { return id }).map(function(id) { return String(parseInt(id) || id) })
  982. }
  983. try {
  984. var pj = JSON.parse(res.data.planJson || '{}')
  985. if (pj.overview) self.planData.overview = pj.overview
  986. if (pj.sections && pj.sections instanceof Array) {
  987. pj.sections.forEach(function(sec) {
  988. if (sec.key && self.sections[sec.key]) {
  989. self.sections[sec.key].content = sec.content || ''
  990. self.sections[sec.key].items = sec.items || []
  991. self.sections[sec.key].confirmed = true
  992. }
  993. })
  994. }
  995. } catch (e) {
  996. if (res.data.planContent) {
  997. self.planData.overview = res.data.planContent.substring(0, 200)
  998. }
  999. }
  1000. }
  1001. }).catch(function() {
  1002. uni.showToast({ title: '方案加载失败', icon: 'none' })
  1003. setTimeout(function() { uni.navigateBack() }, 1500)
  1004. })
  1005. },
  1006. confirmPlanTasks: function() {
  1007. var self = this
  1008. if (!self.planId || self.confirmingTasks) return
  1009. self.confirmingTasks = true
  1010. uni.showLoading({ title: '正在分解任务...' })
  1011. confirmPlan({ planId: self.planId }).then(function(res) {
  1012. uni.hideLoading()
  1013. self.confirmingTasks = false
  1014. if (res && res.code === 200) {
  1015. // 同步当前查看成员为方案执行成员,便于首页/任务中心看到新任务
  1016. var planMemberId = self.getPlanMemberId(null)
  1017. if (planMemberId) {
  1018. uni.setStorageSync('currentMemberId', planMemberId)
  1019. uni.setStorageSync('currentChildId', planMemberId)
  1020. }
  1021. self.loadPlanDetail()
  1022. uni.showModal({
  1023. title: '提示',
  1024. content: '任务已生成,请前往首页-任务中打卡',
  1025. showCancel: false,
  1026. confirmText: '知道了'
  1027. })
  1028. } else {
  1029. uni.showToast({ title: (res && res.message) || '操作失败', icon: 'none' })
  1030. }
  1031. }).catch(function() {
  1032. uni.hideLoading()
  1033. self.confirmingTasks = false
  1034. uni.showToast({ title: '请求失败', icon: 'none' })
  1035. })
  1036. },
  1037. applyForTeacher: function() {
  1038. // 预留:申请规划师接口与业务待定
  1039. },
  1040. getPlanMemberId: function(item) {
  1041. if (item && item.familyMemberId) {
  1042. return item.familyMemberId
  1043. }
  1044. if (this.planDetail && this.planDetail.memberIds) {
  1045. var parts = this.planDetail.memberIds.split(',')
  1046. if (parts.length > 0 && parts[0]) {
  1047. return parseInt(parts[0]) || parts[0]
  1048. }
  1049. }
  1050. var stored = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId')
  1051. return stored || null
  1052. },
  1053. completePlanTask: function(item) {
  1054. var self = this
  1055. if (!item || !item.id || item.status === 'completed' || item.status === 'pending_review') {
  1056. return
  1057. }
  1058. var memberId = self.getPlanMemberId(item)
  1059. if (!memberId) {
  1060. uni.showToast({ title: '未找到执行成员', icon: 'none' })
  1061. return
  1062. }
  1063. uni.showModal({
  1064. title: '完成任务',
  1065. content: '确定完成「' + item.title + '」吗?',
  1066. success: function(res) {
  1067. if (!res.confirm) return
  1068. completeTask(item.id, memberId, '').then(function(r) {
  1069. if (r && r.code === 200) {
  1070. var needReview = r.data && r.data.needReview
  1071. if (needReview) {
  1072. uni.showToast({ title: '已提交,待审核', icon: 'none' })
  1073. } else {
  1074. var points = (r.data && r.data.pointsEarned) || item.points || 0
  1075. uni.showToast({ title: '获得 ' + points + ' 积分', icon: 'success' })
  1076. }
  1077. self.loadPlanTasks()
  1078. } else {
  1079. uni.showToast({ title: (r && r.message) || '完成失败', icon: 'none' })
  1080. }
  1081. }).catch(function() {
  1082. uni.showToast({ title: '完成失败', icon: 'none' })
  1083. })
  1084. }
  1085. })
  1086. },
  1087. goToTaskCenter: function() {
  1088. uni.navigateTo({ url: '/pages/tasks/tasks' })
  1089. },
  1090. reCreate: function() {
  1091. this.step = 1
  1092. this.viewMode = false
  1093. this.planId = null
  1094. this.planDetail = null
  1095. this.taskSubmitted = false
  1096. this.editing = false
  1097. this.sectionFeedback = ''
  1098. this.selectedMemberIds = []
  1099. this.userGoal = ''
  1100. this.planData = {}
  1101. this.sections = {
  1102. nutrition: { content: '', confirmed: false },
  1103. diet: { content: '', confirmed: false },
  1104. exercise: { content: '', confirmed: false }
  1105. }
  1106. // 重置健康问卷
  1107. this.form = { heightCm: '', weightKg: '', bloodPressure: '', bloodGlucose: '', notes: '' }
  1108. this.lipidCholesterol = ''
  1109. this.selectedDiseases = []
  1110. this.diseaseInput = ''
  1111. this.medRows = [{ name: '', startDate: '', durationText: '', time: '' }]
  1112. this.expandedGroups = DISEASE_GROUPS.slice(0, 3).map(function(g) { return g.key })
  1113. this.memberFormMap = {}
  1114. this.activeFormMemberId = ''
  1115. this.pickerMembers = []
  1116. this.goalLen = 0
  1117. if (this.$refs.memberPicker && this.$refs.memberPicker.reload) {
  1118. this.$refs.memberPicker.reload()
  1119. }
  1120. },
  1121. goBack: function() {
  1122. uni.navigateBack()
  1123. }
  1124. }
  1125. }
  1126. </script>
  1127. <style scoped>
  1128. .plan-page {
  1129. min-height: 100vh;
  1130. background: #F5F7FA;
  1131. padding-bottom: 40rpx;
  1132. }
  1133. /* 步骤条 */
  1134. .steps-bar {
  1135. display: flex;
  1136. align-items: center;
  1137. padding: 24rpx 20rpx;
  1138. background: #fff;
  1139. margin-bottom: 4rpx;
  1140. overflow-x: auto;
  1141. }
  1142. .step {
  1143. display: flex;
  1144. flex-direction: column;
  1145. align-items: center;
  1146. flex-shrink: 0;
  1147. min-width: 80rpx;
  1148. }
  1149. .step-num {
  1150. width: 44rpx;
  1151. height: 44rpx;
  1152. border-radius: 22rpx;
  1153. background: #e0e0e0;
  1154. color: #999;
  1155. font-size: 24rpx;
  1156. font-weight: bold;
  1157. display: flex;
  1158. align-items: center;
  1159. justify-content: center;
  1160. transition: all 0.3s;
  1161. }
  1162. .step.active .step-num { background: #F97316; color: #fff; }
  1163. .step.done .step-num { background: #4CAF50; color: #fff; }
  1164. .step-label {
  1165. font-size: 20rpx;
  1166. color: #999;
  1167. white-space: nowrap;
  1168. margin-top: 4rpx;
  1169. }
  1170. .step.active .step-label, .step.done .step-label { color: #333; font-weight: 500; }
  1171. /* 通用区块 */
  1172. .section {
  1173. margin: 20rpx 30rpx;
  1174. background: #fff;
  1175. border-radius: 20rpx;
  1176. padding: 28rpx;
  1177. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  1178. }
  1179. .section-title-row {
  1180. display: flex;
  1181. align-items: center;
  1182. justify-content: space-between;
  1183. margin-bottom: 10rpx;
  1184. }
  1185. .section-title {
  1186. font-size: 30rpx;
  1187. font-weight: bold;
  1188. color: #333;
  1189. }
  1190. .section-badge {
  1191. font-size: 22rpx;
  1192. color: #F97316;
  1193. background: #FFF3E0;
  1194. padding: 4rpx 16rpx;
  1195. border-radius: 20rpx;
  1196. }
  1197. .hint-text {
  1198. font-size: 24rpx;
  1199. color: #999;
  1200. margin-bottom: 20rpx;
  1201. }
  1202. /* 成员选择 chips(与 report-list 同模式) */
  1203. .member-scroll {
  1204. width: 100%;
  1205. white-space: nowrap;
  1206. margin-bottom: 16rpx;
  1207. }
  1208. .member-chips {
  1209. display: inline-flex;
  1210. flex-direction: row;
  1211. padding: 4rpx 0;
  1212. }
  1213. .member-chip {
  1214. display: inline-flex;
  1215. align-items: center;
  1216. justify-content: center;
  1217. padding: 12rpx 28rpx;
  1218. margin-right: 16rpx;
  1219. border-radius: 32rpx;
  1220. background: #F1F5F9;
  1221. border: 2rpx solid transparent;
  1222. }
  1223. .member-chip-on {
  1224. background: #FFF7ED;
  1225. border-color: #F97316;
  1226. }
  1227. .member-chip-text {
  1228. font-size: 26rpx;
  1229. color: #334155;
  1230. font-weight: 600;
  1231. }
  1232. .member-chip-on .member-chip-text {
  1233. color: #F97316;
  1234. }
  1235. /* 目标输入 */
  1236. .goal-textarea {
  1237. width: 100%;
  1238. min-height: 160rpx;
  1239. font-size: 28rpx;
  1240. color: #333;
  1241. background: #F5F7FA;
  1242. border: none;
  1243. border-radius: 16rpx;
  1244. padding: 20rpx 24rpx;
  1245. line-height: 1.7;
  1246. box-sizing: border-box;
  1247. }
  1248. .goal-footer {
  1249. display: flex;
  1250. align-items: center;
  1251. justify-content: space-between;
  1252. margin-top: 16rpx;
  1253. }
  1254. .goal-count { font-size: 24rpx; color: #bbb; }
  1255. /* 按钮 */
  1256. .btn-next, .btn-confirm, .btn-edit, .btn-done, .btn-teacher, .btn-skip, .btn-generate {
  1257. width: 100%;
  1258. height: 88rpx;
  1259. line-height: 88rpx;
  1260. border-radius: 44rpx;
  1261. border: none;
  1262. font-size: 30rpx;
  1263. text-align: center;
  1264. }
  1265. .btn-next, .btn-confirm, .btn-done, .btn-generate {
  1266. background: linear-gradient(135deg, #F97316, #FB923C);
  1267. color: #fff;
  1268. }
  1269. .btn-next[disabled], .btn-confirm[disabled], .btn-generate[disabled] { opacity: 0.4; }
  1270. .btn-edit {
  1271. background: #fff;
  1272. color: #F97316;
  1273. font-size: 28rpx;
  1274. border: 2rpx solid #F97316;
  1275. margin-bottom: 16rpx;
  1276. }
  1277. .btn-teacher {
  1278. background: #fff;
  1279. color: #3B82F6;
  1280. border: 2rpx solid #3B82F6;
  1281. }
  1282. .btn-skip {
  1283. background: #fff;
  1284. color: #999;
  1285. border: 2rpx solid #E5E7EB;
  1286. }
  1287. .full-btn { margin-top: 20rpx; }
  1288. /* 并排按钮容器 */
  1289. .btn-pair {
  1290. display: flex;
  1291. gap: 16rpx;
  1292. margin-top: 20rpx;
  1293. }
  1294. .btn-pair .btn-confirm,
  1295. .btn-pair .btn-teacher,
  1296. .btn-pair .btn-skip,
  1297. .btn-pair .btn-generate {
  1298. flex: 1;
  1299. margin-top: 0;
  1300. }
  1301. /* 加载状态 */
  1302. .loading-state {
  1303. display: flex;
  1304. flex-direction: column;
  1305. align-items: center;
  1306. padding: 80rpx 20rpx;
  1307. }
  1308. .loading-icon { font-size: 80rpx; margin-bottom: 20rpx; }
  1309. .loading-text { font-size: 30rpx; color: #333; margin-bottom: 8rpx; }
  1310. .loading-hint { font-size: 24rpx; color: #999; }
  1311. /* 总览卡片 */
  1312. .overview-card {
  1313. background: #FFF7ED;
  1314. border-radius: 16rpx;
  1315. padding: 24rpx;
  1316. border-left: 4rpx solid #F97316;
  1317. margin-bottom: 20rpx;
  1318. }
  1319. .overview-text {
  1320. font-size: 28rpx;
  1321. color: #555;
  1322. line-height: 1.8;
  1323. }
  1324. /* section 预览列表 */
  1325. .section-preview-list {
  1326. display: flex;
  1327. flex-direction: column;
  1328. gap: 16rpx;
  1329. margin: 20rpx 0;
  1330. }
  1331. .section-preview-item {
  1332. display: flex;
  1333. align-items: center;
  1334. gap: 16rpx;
  1335. padding: 20rpx;
  1336. background: #FAFAFA;
  1337. border-radius: 16rpx;
  1338. border: 2rpx solid #eee;
  1339. }
  1340. .sec-icon { font-size: 40rpx; }
  1341. .sec-info { flex: 1; }
  1342. .sec-title { font-size: 28rpx; color: #333; font-weight: 600; display: block; }
  1343. .sec-status { font-size: 22rpx; color: #999; }
  1344. .sec-status.done { color: #4CAF50; }
  1345. .sec-arrow { color: #ccc; font-size: 28rpx; }
  1346. /* content 展示/编辑 */
  1347. .content-display {
  1348. background: #F5F7FA;
  1349. border-radius: 16rpx;
  1350. padding: 24rpx;
  1351. margin-bottom: 20rpx;
  1352. }
  1353. .content-text {
  1354. font-size: 28rpx;
  1355. color: #333;
  1356. line-height: 1.8;
  1357. white-space: pre-wrap;
  1358. word-break: break-all;
  1359. }
  1360. .edit-textarea {
  1361. width: 100%;
  1362. min-height: 300rpx;
  1363. font-size: 28rpx;
  1364. color: #333;
  1365. background: #F5F7FA;
  1366. border: 2rpx solid #F97316;
  1367. border-radius: 16rpx;
  1368. padding: 20rpx 24rpx;
  1369. line-height: 1.7;
  1370. box-sizing: border-box;
  1371. margin-bottom: 16rpx;
  1372. }
  1373. .feedback-input {
  1374. width: 100%;
  1375. height: 80rpx;
  1376. background: #FFF7ED;
  1377. border: 2rpx solid #F97316;
  1378. border-radius: 40rpx;
  1379. padding: 0 24rpx;
  1380. font-size: 26rpx;
  1381. color: #333;
  1382. box-sizing: border-box;
  1383. }
  1384. .section-actions {
  1385. display: flex;
  1386. gap: 16rpx;
  1387. margin-top: 16rpx;
  1388. }
  1389. .section-actions .btn-edit, .section-actions .btn-confirm {
  1390. flex: 1;
  1391. margin-bottom: 0;
  1392. }
  1393. .back-link {
  1394. text-align: center;
  1395. font-size: 26rpx;
  1396. color: #999;
  1397. margin-top: 20rpx;
  1398. }
  1399. .back-link:active { opacity: 0.7; }
  1400. /* 最终确认 */
  1401. .final-review {
  1402. display: flex;
  1403. flex-direction: column;
  1404. gap: 16rpx;
  1405. margin: 20rpx 0;
  1406. }
  1407. .review-item {
  1408. background: #FAFAFA;
  1409. border-radius: 16rpx;
  1410. padding: 20rpx;
  1411. border: 2rpx solid #eee;
  1412. }
  1413. .review-title {
  1414. font-size: 28rpx;
  1415. font-weight: 600;
  1416. color: #333;
  1417. display: block;
  1418. margin-bottom: 8rpx;
  1419. }
  1420. .review-preview {
  1421. font-size: 24rpx;
  1422. color: #888;
  1423. line-height: 1.6;
  1424. }
  1425. /* 查看模式 */
  1426. .plan-view-section { margin-top: 0; }
  1427. .plan-meta {
  1428. display: flex;
  1429. flex-direction: column;
  1430. gap: 6rpx;
  1431. padding: 16rpx 20rpx;
  1432. background: #FFF7ED;
  1433. border-radius: 12rpx;
  1434. margin-bottom: 20rpx;
  1435. }
  1436. .plan-meta-item { font-size: 24rpx; color: #92400E; }
  1437. .plan-preview {
  1438. background: #F5F7FA;
  1439. border-radius: 16rpx;
  1440. padding: 24rpx;
  1441. margin-bottom: 20rpx;
  1442. }
  1443. .plan-preview-text {
  1444. font-size: 28rpx;
  1445. color: #333;
  1446. line-height: 1.8;
  1447. white-space: pre-wrap;
  1448. }
  1449. .plan-confirm-actions {
  1450. margin-bottom: 20rpx;
  1451. }
  1452. /* 成功遮罩 */
  1453. .success-overlay {
  1454. position: fixed;
  1455. top: 0; left: 0; right: 0; bottom: 0;
  1456. background: rgba(0,0,0,0.55);
  1457. display: flex;
  1458. align-items: center;
  1459. justify-content: center;
  1460. z-index: 100;
  1461. }
  1462. .success-card {
  1463. background: #fff;
  1464. border-radius: 32rpx;
  1465. padding: 60rpx 48rpx;
  1466. display: flex;
  1467. flex-direction: column;
  1468. align-items: center;
  1469. width: 560rpx;
  1470. }
  1471. .success-icon { font-size: 100rpx; color: #4CAF50; margin-bottom: 20rpx; }
  1472. .success-title { font-size: 36rpx; font-weight: bold; color: #333; margin-bottom: 12rpx; }
  1473. .success-sub { font-size: 28rpx; color: #888; margin-bottom: 40rpx; }
  1474. /* ===== 健康问卷样式 (从 health-status-form.vue 移植) ===== */
  1475. .form-section {
  1476. margin: 0 0 24rpx;
  1477. background: #fff;
  1478. border-radius: 16rpx;
  1479. padding: 24rpx;
  1480. }
  1481. .form-title {
  1482. font-size: 30rpx;
  1483. font-weight: bold;
  1484. color: #333;
  1485. margin-bottom: 20rpx;
  1486. }
  1487. .form-row {
  1488. display: flex;
  1489. align-items: center;
  1490. justify-content: space-between;
  1491. margin-bottom: 20rpx;
  1492. }
  1493. .form-label {
  1494. font-size: 28rpx;
  1495. color: #555;
  1496. }
  1497. .form-input {
  1498. flex: 1;
  1499. margin-left: 30rpx;
  1500. height: 68rpx;
  1501. background: #F5F7FA;
  1502. border-radius: 10rpx;
  1503. padding: 0 20rpx;
  1504. font-size: 28rpx;
  1505. text-align: right;
  1506. }
  1507. /* 疾病标签 */
  1508. .disease-group { margin-bottom: 8rpx; }
  1509. .group-head {
  1510. display: flex;
  1511. align-items: center;
  1512. justify-content: space-between;
  1513. padding: 12rpx 0;
  1514. }
  1515. .group-title { font-size: 28rpx; font-weight: bold; color: #333; }
  1516. .group-arrow { font-size: 24rpx; color: #999; }
  1517. .risk-badge { margin-left: 6rpx; color: #E64340; font-size: 24rpx; }
  1518. .tag-wrap { display: flex; flex-wrap: wrap; }
  1519. .tag-item {
  1520. padding: 12rpx 24rpx;
  1521. background: #F5F7FA;
  1522. border-radius: 30rpx;
  1523. font-size: 26rpx;
  1524. color: #666;
  1525. margin: 0 16rpx 16rpx 0;
  1526. }
  1527. .tag-active { background: #F97316; color: #fff; }
  1528. /* 已选疾病 */
  1529. .selected-area { margin-top: 16rpx; padding: 16rpx; background: #FFF7E6; border-radius: 12rpx; }
  1530. .selected-title { font-size: 26rpx; color: #8A6D3B; margin-bottom: 8rpx; }
  1531. .sel-row {
  1532. display: flex;
  1533. align-items: center;
  1534. justify-content: space-between;
  1535. padding: 10rpx 0;
  1536. border-bottom: 1rpx solid #F0E6D0;
  1537. }
  1538. .sel-name { font-size: 28rpx; color: #333; flex: 1; }
  1539. .sel-diag { font-size: 26rpx; color: #F97316; }
  1540. .sel-del { font-size: 30rpx; color: #E64340; margin-left: 16rpx; }
  1541. /* 自定义疾病输入 */
  1542. .custom-row { display: flex; align-items: center; margin-top: 8rpx; }
  1543. .custom-input {
  1544. flex: 1;
  1545. height: 64rpx;
  1546. background: #F5F7FA;
  1547. border-radius: 10rpx;
  1548. padding: 0 20rpx;
  1549. font-size: 26rpx;
  1550. margin-right: 20rpx;
  1551. }
  1552. .add-link { font-size: 28rpx; color: #F97316; }
  1553. /* 药物行 */
  1554. .med-row {
  1555. border: 1rpx solid #F0F0F0;
  1556. border-radius: 12rpx;
  1557. padding: 16rpx;
  1558. margin-bottom: 16rpx;
  1559. }
  1560. .med-line {
  1561. display: flex;
  1562. align-items: center;
  1563. margin-bottom: 12rpx;
  1564. }
  1565. .med-line:last-child { margin-bottom: 0; }
  1566. .med-name {
  1567. flex: 1;
  1568. height: 64rpx;
  1569. background: #F5F7FA;
  1570. border-radius: 10rpx;
  1571. padding: 0 16rpx;
  1572. font-size: 26rpx;
  1573. margin-right: 12rpx;
  1574. }
  1575. .med-picker {
  1576. font-size: 26rpx;
  1577. color: #F97316;
  1578. padding: 8rpx 16rpx;
  1579. background: #F5F7FA;
  1580. border-radius: 10rpx;
  1581. }
  1582. .med-forget { font-size: 26rpx; color: #999; margin-left: 16rpx; }
  1583. .med-duration { font-size: 26rpx; color: #8A6D3B; }
  1584. .med-time {
  1585. flex: 1;
  1586. height: 64rpx;
  1587. background: #F5F7FA;
  1588. border-radius: 10rpx;
  1589. padding: 0 16rpx;
  1590. font-size: 26rpx;
  1591. margin-right: 12rpx;
  1592. }
  1593. .med-del { font-size: 26rpx; color: #E64340; }
  1594. /* 备注 */
  1595. .notes-area {
  1596. width: 100%;
  1597. height: 160rpx;
  1598. background: #F5F7FA;
  1599. border-radius: 10rpx;
  1600. padding: 16rpx 20rpx;
  1601. font-size: 26rpx;
  1602. box-sizing: border-box;
  1603. }
  1604. /* 空状态 */
  1605. .empty-state {
  1606. width: 100%;
  1607. display: flex;
  1608. flex-direction: column;
  1609. align-items: center;
  1610. padding: 40rpx 0 24rpx;
  1611. box-sizing: border-box;
  1612. }
  1613. .empty-text { font-size: 26rpx; color: #999; }
  1614. .member-load-hint {
  1615. font-size: 22rpx;
  1616. color: #F97316;
  1617. margin-bottom: 12rpx;
  1618. }
  1619. .empty-action {
  1620. margin-top: 20rpx;
  1621. padding: 12rpx 32rpx;
  1622. font-size: 26rpx;
  1623. color: #F97316;
  1624. border: 2rpx solid #F97316;
  1625. border-radius: 32rpx;
  1626. }
  1627. </style>