| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160 |
- <template>
- <view class="survey-wrap">
- <!-- 顶部进度条 -->
- <view class="progress-bar">
- <view class="progress-track">
- <view class="progress-fill" :style="'width:' + progressPercent + '%'"></view>
- </view>
- <view class="progress-labels">
- <text v-for="(m, i) in activeModules" :key="m"
- class="progress-label" :class="{ 'progress-label-active': currentStep > i }">{{ moduleTitles[m] }}</text>
- </view>
- </view>
- <!-- 步骤内容 -->
- <view class="content">
- <!-- ===== Step: 基本情况 basic ===== -->
- <view v-if="currentModule === 'basic'">
- <view class="step-head">
- <text class="step-icon">📏</text>
- <view class="step-head-text">
- <text class="step-title">基本情况</text>
- <text class="step-sub">身高、体重、血压、血糖、总胆固醇(快捷标签可一键填入参考值,再手改)</text>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">身高(cm)</text>
- <view class="quick-chips">
- <view class="quick-chip" :class="{ 'quick-chip-on': heightQuick === 'normal' }" @tap="setHeightQuick('normal')"><text>正常</text></view>
- <view class="quick-chip" :class="{ 'quick-chip-on': heightQuick === 'high' }" @tap="setHeightQuick('high')"><text>偏高</text></view>
- <view class="quick-chip" :class="{ 'quick-chip-on': heightQuick === 'low' }" @tap="setHeightQuick('low')"><text>偏低</text></view>
- </view>
- </view>
- <input class="num-input" type="digit" v-model="innerForm.heightCm" placeholder="如 170" />
- <text class="unit-hint">单位:厘米</text>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">体重(kg)</text>
- <view class="quick-chips">
- <view class="quick-chip" :class="{ 'quick-chip-on': weightQuick === 'normal' }" @tap="setWeightQuick('normal')"><text>正常</text></view>
- <view class="quick-chip" :class="{ 'quick-chip-on': weightQuick === 'high' }" @tap="setWeightQuick('high')"><text>偏重</text></view>
- <view class="quick-chip" :class="{ 'quick-chip-on': weightQuick === 'low' }" @tap="setWeightQuick('low')"><text>偏轻</text></view>
- </view>
- </view>
- <input class="num-input" type="digit" v-model="innerForm.weightKg" placeholder="如 60" />
- <text class="unit-hint">单位:公斤</text>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">血压</text>
- <view class="quick-chips">
- <view class="quick-chip" :class="{ 'quick-chip-on': bpQuick === 'normal' }" @tap="setBpQuick('normal')"><text>正常</text></view>
- <view class="quick-chip" :class="{ 'quick-chip-on': bpQuick === 'high' }" @tap="setBpQuick('high')"><text>偏高</text></view>
- <view class="quick-chip" :class="{ 'quick-chip-on': bpQuick === 'low' }" @tap="setBpQuick('low')"><text>偏低</text></view>
- </view>
- </view>
- <input class="num-input" type="text" v-model="innerForm.bloodPressure" placeholder="如 120/80" />
- <text class="unit-hint">格式:收缩压/舒张压(mmHg)</text>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">空腹血糖(mmol/L)</text>
- <view class="quick-chips">
- <view class="quick-chip" :class="{ 'quick-chip-on': glucoseQuick === 'normal' }" @tap="setGlucoseQuick('normal')"><text>正常</text></view>
- <view class="quick-chip" :class="{ 'quick-chip-on': glucoseQuick === 'high' }" @tap="setGlucoseQuick('high')"><text>偏高</text></view>
- <view class="quick-chip" :class="{ 'quick-chip-on': glucoseQuick === 'low' }" @tap="setGlucoseQuick('low')"><text>偏低</text></view>
- </view>
- </view>
- <input class="num-input" type="digit" v-model="innerForm.bloodGlucose" placeholder="如 5.0" />
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">总胆固醇(mmol/L)</text>
- <view class="quick-chips">
- <view class="quick-chip" :class="{ 'quick-chip-on': lipidQuick === 'normal' }" @tap="setLipidQuick('normal')"><text>正常</text></view>
- <view class="quick-chip" :class="{ 'quick-chip-on': lipidQuick === 'high' }" @tap="setLipidQuick('high')"><text>偏高</text></view>
- </view>
- </view>
- <input class="num-input" type="digit" v-model="innerForm.lipidCholesterol" placeholder="如 4.5" />
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">其他补充(选填)</text></view>
- <textarea class="notes-area" v-model="innerForm.notes" placeholder="其他想补充的健康信息" :maxlength="500" />
- </view>
- </view>
- <!-- ===== Step: 当前状态 status ===== -->
- <view v-if="currentModule === 'status'">
- <view class="step-head">
- <text class="step-icon">🩺</text>
- <view class="step-head-text">
- <text class="step-title">当前状态</text>
- <text class="step-sub">特殊身体状态;按性别/年龄显示人群专属问题</text>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">特殊状态</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in currentStatusOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.currentStatus === opt }"
- @tap="setLifestyle('currentStatus', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- <!-- 女性专属 -->
- <view v-if="isFemale">
- <view class="card">
- <view class="card-head"><text class="card-title">月经周期</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in menstrualOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.menstrualCycle === opt }"
- @tap="setLifestyle('menstrualCycle', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">特殊时期</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in specialPeriodOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.specialPeriod === opt }"
- @tap="setLifestyle('specialPeriod', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- </view>
- <!-- 儿童专属 -->
- <view v-if="isChild">
- <view class="card">
- <view class="card-head"><text class="card-title">生长发育</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in growthOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.growthDevelopment === opt }"
- @tap="setLifestyle('growthDevelopment', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">挑食情况</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in pickyEatingOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.pickyEating === opt }"
- @tap="setLifestyle('pickyEating', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- </view>
- <!-- 老年专属 -->
- <view v-if="isElderly">
- <view class="card">
- <view class="card-head"><text class="card-title">骨质疏松风险</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in osteoporosisOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.osteoporosisRisk === opt }"
- @tap="setLifestyle('osteoporosisRisk', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">跌倒风险</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in fallRiskOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.fallRisk === opt }"
- @tap="setLifestyle('fallRisk', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- </view>
- <!-- 青少年专属 -->
- <view v-if="isTeenager">
- <view class="card">
- <view class="card-head"><text class="card-title">屏幕时间</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in screenTimeOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.screenTime === opt }"
- @tap="setLifestyle('screenTime', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">情绪状态</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in teenMoodOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.teenMood === opt }"
- @tap="setLifestyle('teenMood', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- </view>
- </view>
- <!-- ===== Step: 饮食偏好 diet ===== -->
- <view v-if="currentModule === 'diet'">
- <view class="step-head">
- <text class="step-icon">🍽️</text>
- <view class="step-head-text">
- <text class="step-title">饮食偏好</text>
- <text class="step-sub">忌口、口味、菜系、烹饪方式、饮食结构(过敏原在最后一步单独采集)</text>
- </view>
- </view>
- <view class="pref-none-row">
- <button class="pref-none-btn" :class="{ 'pref-none-btn-active': innerDiet.noPreference }"
- @tap="setNoPreference">{{ innerDiet.noPreference ? '已选:无特殊偏好' : '无特殊偏好,直接下一步' }}</button>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">绝对忌口</text><text class="card-hint">多选</text></view>
- <view class="chip-row-wrap">
- <view v-for="item in presetAvoid" :key="item" class="chip-item-wrap"
- :class="{ 'chip-active': innerDiet.absoluteAvoid.indexOf(item) >= 0 }"
- @tap="toggleDietChip('absoluteAvoid', item)"><text>{{ item }}</text></view>
- </view>
- <view class="custom-row" v-if="customField === 'absoluteAvoid'">
- <input class="custom-input" placeholder="输入其他忌口" v-model="customInput" confirm-type="done" @confirm="addCustom" />
- <view class="custom-confirm" @tap="addCustom"><text>添加</text></view>
- </view>
- <view class="custom-add" v-else @tap="openCustom('absoluteAvoid')"><text>+ 自定义添加</text></view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">必吃食物</text><text class="card-hint">多选</text></view>
- <view class="chip-row-wrap">
- <view v-for="item in presetMustEat" :key="item" class="chip-item-wrap"
- :class="{ 'chip-active': innerDiet.mustEat.indexOf(item) >= 0 }"
- @tap="toggleDietChip('mustEat', item)"><text>{{ item }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">口味偏好</text><text class="card-hint">多选</text></view>
- <view class="chip-row-wrap">
- <view v-for="item in presetFlavor" :key="item" class="chip-item-wrap"
- :class="{ 'chip-active': innerDiet.flavorPref.indexOf(item) >= 0 }"
- @tap="toggleDietChip('flavorPref', item)"><text>{{ item }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">菜系偏好</text><text class="card-hint">多选</text></view>
- <view class="chip-row-wrap">
- <view v-for="item in presetCuisine" :key="item" class="chip-item-wrap"
- :class="{ 'chip-active': innerDiet.cuisinePref.indexOf(item) >= 0 }"
- @tap="toggleDietChip('cuisinePref', item)"><text>{{ item }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">烹饪方式</text><text class="card-hint">多选</text></view>
- <view class="chip-row-wrap">
- <view v-for="item in presetCooking" :key="item" class="chip-item-wrap"
- :class="{ 'chip-active': innerDiet.cookingMethods.indexOf(item) >= 0 }"
- @tap="toggleDietChip('cookingMethods', item)"><text>{{ item }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">健康目标</text><text class="card-hint">多选</text></view>
- <view class="chip-row-wrap">
- <view v-for="item in presetGoals" :key="item" class="chip-item-wrap"
- :class="{ 'chip-active': innerDiet.healthGoals.indexOf(item) >= 0 }"
- @tap="toggleDietChip('healthGoals', item)"><text>{{ item }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">辣度</text><text class="card-hint">{{ spiceLabels[innerDiet.spiceLevel || 0] }}</text></view>
- <view class="slider-row">
- <slider :value="innerDiet.spiceLevel || 0" min="0" max="5" step="1" activeColor="#FF8C42"
- @changing="onSpiceChanging" @change="onSpiceChange" />
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">宗教饮食</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in religiousOptions" :key="opt.value" class="chip-item-wrap"
- :class="{ 'chip-active': innerDiet.religiousDiet === opt.value }"
- @tap="setDietValue('religiousDiet', opt.value)"><text>{{ opt.label }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">饮食结构现状</text><text class="card-hint">单选</text></view>
- <view class="struct-row"><text class="struct-label">每日蔬果量</text>
- <view class="chip-row-wrap">
- <view v-for="opt in vegFruitOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': innerDiet.vegetableFruit === opt }"
- @tap="setDietValue('vegetableFruit', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- <view class="struct-row"><text class="struct-label">乳制品摄入</text>
- <view class="chip-row-wrap">
- <view v-for="opt in dairyOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': innerDiet.dairy === opt }"
- @tap="setDietValue('dairy', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- <view class="struct-row"><text class="struct-label">含糖饮料频率</text>
- <view class="chip-row-wrap">
- <view v-for="opt in sugaryDrinkOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': innerDiet.sugaryDrinks === opt }"
- @tap="setDietValue('sugaryDrinks', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- </view>
- </view>
- <!-- ===== Step: 疾病史 disease ===== -->
- <view v-if="currentModule === 'disease'">
- <view class="step-head">
- <text class="step-icon">📋</text>
- <view class="step-head-text">
- <text class="step-title">疾病史</text>
- <text class="step-sub">选择过往确诊疾病,可设确诊年月(★ 为重点关注项)</text>
- </view>
- </view>
- <view v-for="(group, gi) in diseaseGroups" :key="group.key" class="card">
- <view class="card-head" @tap="toggleGroup(group.key)">
- <text class="card-title">{{ group.title }}</text>
- <text class="group-arrow">{{ expandedGroups.indexOf(group.key) >= 0 ? '收起' : '展开' }}</text>
- </view>
- <view v-if="expandedGroups.indexOf(group.key) >= 0" class="chip-row-wrap">
- <view v-for="(item, ii) in group.items" :key="getDiseaseKey(gi, ii)" class="chip-item-wrap"
- :class="{ 'chip-active': selectedDiseaseNames.indexOf(item.name) >= 0 }"
- @tap="toggleDisease(item.name)">
- <text>{{ item.name }}</text>
- <text v-if="item.risk" class="risk-badge">★</text>
- </view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">自定义疾病</text></view>
- <view class="custom-row">
- <input class="custom-input" type="text" v-model="diseaseInput" placeholder="输入其他疾病" />
- <view class="custom-confirm" @tap="addCustomDisease"><text>添加</text></view>
- </view>
- </view>
- <view class="card" v-if="innerForm.selectedDiseases.length > 0">
- <view class="card-head"><text class="card-title">已选疾病({{ innerForm.selectedDiseases.length }})</text></view>
- <view v-for="(item, si) in innerForm.selectedDiseases" :key="getSelKey(si)" class="sel-row">
- <text class="sel-name">{{ item.name }}</text>
- <picker mode="date" fields="month" :value="item.diagnosedAt || currentMonth" :end="monthEnd" @change="onDiagChange(si, $event)">
- <view class="sel-diag">{{ item.diagnosedAt ? item.diagnosedAt : '设确诊时间' }}</view>
- </picker>
- <text class="sel-del" @tap="removeSelected(si)">×</text>
- </view>
- </view>
- </view>
- <!-- ===== Step: 排便及运动 bowel ===== -->
- <view v-if="currentModule === 'bowel'">
- <view class="step-head">
- <text class="step-icon">🚽</text>
- <view class="step-head-text">
- <text class="step-title">排便及运动</text>
- <text class="step-sub">排便习惯 + 运动频率与每次时长</text>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">排便频率</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in bowelFreqOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.bowelFrequency === opt }"
- @tap="setLifestyle('bowelFrequency', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">粪便性状</text><text class="card-hint">对照图片单选</text></view>
- <view class="stool-tip">参照布里斯托大便分类法,选择最接近您多数情况的一项</view>
- <view class="stool-grid">
- <view v-for="opt in bowelFormOptions" :key="opt.value" class="stool-item"
- :class="{ 'stool-active': lifestyle.bowelForm === opt.value }"
- @tap="setLifestyle('bowelForm', opt.value)">
- <image class="stool-img" :src="opt.img" mode="aspectFit" />
- <text class="stool-name">{{ opt.label }}</text>
- <text class="stool-desc">{{ opt.desc }}</text>
- </view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">运动频率</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in exerciseFreqOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.exerciseFrequency === opt }"
- @tap="setLifestyle('exerciseFrequency', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">每次运动时长</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in exerciseDurationOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.exerciseDuration === opt }"
- @tap="setLifestyle('exerciseDuration', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- </view>
- <!-- ===== Step: 睡眠情况 sleep ===== -->
- <view v-if="currentModule === 'sleep'">
- <view class="step-head">
- <text class="step-icon">😴</text>
- <view class="step-head-text">
- <text class="step-title">睡眠情况</text>
- <text class="step-sub">睡眠时长与入睡时间</text>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">睡眠时长</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in sleepDurationOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.sleepDuration === opt }"
- @tap="setLifestyle('sleepDuration', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">入睡时间</text><text class="card-hint">单选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in bedtimeOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.bedtime === opt }"
- @tap="setLifestyle('bedtime', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- </view>
- <!-- ===== Step: 用药情况 med ===== -->
- <view v-if="currentModule === 'med'">
- <view class="step-head">
- <text class="step-icon">💊</text>
- <view class="step-head-text">
- <text class="step-title">用药情况</text>
- <text class="step-sub">当前在服药物;如无可勾选「无长期用药」</text>
- </view>
- </view>
- <view class="card">
- <view class="quick-chip" :class="{ 'quick-chip-on': innerForm.noMedication }" @tap="toggleNoMedication">
- <text>{{ innerForm.noMedication ? '✓ 已选' : '无长期用药' }}</text>
- </view>
- <text class="unit-hint" v-if="innerForm.noMedication">已标记无长期用药,可跳过药物填写</text>
- </view>
- <view v-if="!innerForm.noMedication" v-for="(row, idx) in innerForm.medRows" :key="getMedKey(idx)" class="card">
- <view class="card-head"><text class="card-title">用药 {{ idx + 1 }}</text>
- <text class="card-del" @tap="removeMed(idx)">删除</text>
- </view>
- <view class="card-row"><text class="card-label">药名</text>
- <input class="card-input" type="text" v-model="row.name" placeholder="药物品牌,如 立普妥" /></view>
- <view class="card-row"><text class="card-label">开始年月</text>
- <picker mode="date" fields="month" :value="row.startDate || currentMonth" :end="monthEnd" @change="onMedStartChange(idx, $event)">
- <view class="picker-val">{{ row.startDate ? row.startDate : '选择开始日期' }}</view>
- </picker>
- <text class="link-forget" @tap="onForgetClick(idx)">记不清</text>
- </view>
- <view class="card-row" v-if="row.durationText"><text class="card-label">已服时长</text>
- <text class="card-value">{{ row.durationText }}</text></view>
- <view class="card-row"><text class="card-label">服药时间</text>
- <input class="card-input" type="text" v-model="row.time" placeholder="如 早饭后一次" /></view>
- </view>
- <view class="add-row" v-if="!innerForm.noMedication" @tap="addMed"><text class="add-icon">+</text><text class="add-text">添加药物/治疗</text></view>
- </view>
- <!-- ===== Step: 过敏源 allergy ===== -->
- <view v-if="currentModule === 'allergy'">
- <view class="step-head">
- <text class="step-icon">⚠️</text>
- <view class="step-head-text">
- <text class="step-title">过敏源</text>
- <text class="step-sub">过敏原与过敏症状均可多选,可自定义添加</text>
- </view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">过敏原</text><text class="card-hint">多选</text></view>
- <view class="chip-row-wrap">
- <view v-for="item in presetAllergies" :key="item" class="chip-item-wrap"
- :class="{ 'chip-active': innerDiet.allergies.indexOf(item) >= 0 }"
- @tap="toggleDietChip('allergies', item)"><text>{{ item }}</text></view>
- </view>
- <view class="custom-row" v-if="customField === 'allergies'">
- <input class="custom-input" placeholder="输入其他过敏原" v-model="customInput" confirm-type="done" @confirm="addCustom" />
- <view class="custom-confirm" @tap="addCustom"><text>添加</text></view>
- </view>
- <view class="custom-add" v-else @tap="openCustom('allergies')"><text>+ 自定义添加</text></view>
- </view>
- <view class="card">
- <view class="card-head"><text class="card-title">过敏表现</text><text class="card-hint">多选</text></view>
- <view class="chip-row-wrap">
- <view v-for="opt in allergyOptions" :key="opt" class="chip-item-wrap"
- :class="{ 'chip-active': lifestyle.allergySymptoms.indexOf(opt) >= 0 }"
- @tap="toggleLifestyleArray('allergySymptoms', opt)"><text>{{ opt }}</text></view>
- </view>
- </view>
- </view>
- </view>
- <!-- 底部固定栏 -->
- <view class="footer-bar">
- <button class="btn-prev" v-if="currentStep > 1" @tap="prevStep">上一步</button>
- <button class="btn-next" v-if="currentStep < activeModules.length" @tap="nextStep">下一步</button>
- <button class="btn-done" v-else :disabled="submitting" @tap="onSubmit">
- {{ submitting ? '保存中...' : '完成' }}
- </button>
- </view>
- </view>
- </template>
- <script>
- import { BOWEL_FORM_OPTIONS, normalizeBowelForm } from '@/utils/bowel-form.js'
- // ===== 预设常量(与旧子组件 / preferences.vue 保持一致) =====
- var PRESET_ALLERGIES = ['海鲜', '花生', '乳糖', '鸡蛋', '牛奶', '豆类', '麸质', '坚果', '芒果', '其他']
- var PRESET_AVOID = ['牛肉', '羊肉', '猪肉', '内脏', '油腻', '辛辣', '生冷', '腌制']
- var PRESET_FLAVOR = ['酸', '甜', '咸', '鲜', '苦', '清淡', '浓郁']
- var PRESET_CUISINE = ['川', '粤', '鲁', '淮扬', '日料', '西餐', '东南亚', '韩餐']
- var PRESET_COOKING = ['蒸', '煮', '炒', '烤', '炖', '凉拌', '煎', '炸']
- var PRESET_MUSTEAT = ['蔬菜', '水果', '粗粮', '鱼类', '鸡蛋', '豆制品', '菌菇']
- var PRESET_GOALS = ['减脂', '增肌', '控糖', '肠道调理', '增强免疫']
- var VALID_MODULES = ['basic', 'status', 'diet', 'disease', 'bowel', 'sleep', 'med', 'allergy']
- // 8 组 53 项预置疾病(与旧 MedicalHistoryStep.vue 一致,risk=true 为重点关注项)
- var DISEASE_GROUPS = [
- { key: 'cardio', title: '心血管代谢', items: [
- { name: '高血压', risk: false },
- { name: '糖尿病', risk: false },
- { name: '血脂异常', risk: false },
- { name: '冠心病', risk: false },
- { name: '脑卒中', risk: false },
- { name: '心力衰竭', risk: false },
- { name: '心房颤动', risk: false },
- { name: '慢阻肺', risk: false },
- { name: '心脑血管疾病', risk: true },
- { name: 'II型糖尿病', risk: true },
- { name: '肥胖', risk: true },
- { name: '超重', risk: false },
- { name: '高尿酸血症', risk: false },
- { name: '骨质疏松', risk: false }
- ]},
- { key: 'digest', title: '消化系统', items: [
- { name: '幽门螺杆菌感染', risk: false },
- { name: '脂肪肝', risk: false },
- { name: '慢性胃炎', risk: false },
- { name: '胃食管反流', risk: false },
- { name: '肠易激综合征', risk: true },
- { name: '炎症性肠炎', risk: true },
- { name: '慢性便秘', risk: true },
- { name: '感染性腹泻', risk: true },
- { name: '胆结石', risk: false },
- { name: '炎症性肠病', risk: false },
- { name: '肝病', risk: true },
- { name: '胃病', risk: true },
- { name: '胆病', risk: true },
- { name: '肾病', risk: true },
- { name: '腹胀', risk: true }
- ]},
- { key: 'immune', title: '过敏免疫', items: [
- { name: '过敏性鼻炎', risk: true },
- { name: '哮喘', risk: false },
- { name: '湿疹', risk: false },
- { name: '食物过敏', risk: false },
- { name: '自体免疫病', risk: true }
- ]},
- { key: 'neuro', title: '精神神经', items: [
- { name: '抑郁症', risk: true },
- { name: '失眠', risk: true },
- { name: '焦虑障碍', risk: false },
- { name: '偏头痛', risk: false },
- { name: '自闭症', risk: true }
- ]},
- { key: 'resp', title: '呼吸系统', items: [
- { name: '肺部感染或疾病', risk: true },
- { name: '肺部疾病', risk: true }
- ]},
- { key: 'tumor', title: '肿瘤', items: [
- { name: '结直肠癌', risk: true },
- { name: '肺癌', risk: false },
- { name: '甲状腺癌', risk: false },
- { name: '肝癌', risk: false },
- { name: '胃癌', risk: false },
- { name: '乳腺癌', risk: false },
- { name: '食管癌', risk: false }
- ]},
- { key: 'child', title: '儿童青少年', items: [
- { name: '近视', risk: false },
- { name: '龋齿', risk: false },
- { name: '注意缺陷多动障碍', risk: false },
- { name: '甲状腺疾病', risk: true }
- ]},
- { key: 'other', title: '其他', items: [
- { name: '缺铁性贫血', risk: false },
- { name: '贫血', risk: false }
- ]}
- ]
- // 新采集的 lifestyle key(旧数据里可能没有,默认空值)
- var LIFESTYLE_KEYS = [
- 'currentStatus', 'menstrualCycle', 'specialPeriod', 'growthDevelopment', 'pickyEating',
- 'osteoporosisRisk', 'fallRisk', 'screenTime', 'teenMood',
- 'bowelFrequency', 'bowelForm', 'exerciseFrequency', 'exerciseDuration',
- 'sleepDuration', 'bedtime', 'allergySymptoms',
- 'mood', 'antibioticHistory'
- ]
- // lifestyle key → 所属调研模块(用于「主动清空」时判断是否允许下发 null)
- var LIFESTYLE_MODULE_MAP = {
- currentStatus: 'status', menstrualCycle: 'status', specialPeriod: 'status',
- growthDevelopment: 'status', pickyEating: 'status', osteoporosisRisk: 'status',
- fallRisk: 'status', screenTime: 'status', teenMood: 'status',
- bowelFrequency: 'bowel', bowelForm: 'bowel', exerciseFrequency: 'bowel', exerciseDuration: 'bowel',
- sleepDuration: 'sleep', bedtime: 'sleep',
- allergySymptoms: 'allergy',
- mood: '', antibioticHistory: ''
- }
- export default {
- name: 'ComprehensiveSurvey',
- props: {
- // 启用模块 key 列表(顺序决定步骤顺序;空/全非法 → 全量 8 模块)
- modules: { type: Array, default: function() { return [] } },
- // health_status 快照(结构化字段,lifestyle 为对象非 JSON 串)
- form: { type: Object, default: function() { return {} } },
- // diet_preferences 快照(13 key)
- diet: { type: Object, default: function() { return {} } },
- // 成员画像(gender/age 驱动条件渲染)
- profile: { type: Object, default: function() { return {} } },
- // 外部提交态
- submitting: { type: Boolean, default: false }
- },
- data: function() {
- return {
- currentStep: 1,
- innerForm: {},
- innerDiet: {},
- lifestyle: {},
- // 初始快照的疾病/用药数量(用于区分「主动清空」与「原本为空」)
- _initDiseaseCount: 0,
- _initMedCount: 0,
- currentMonth: '',
- monthEnd: '',
- expandedGroups: [],
- // 自定义输入
- customField: '',
- customInput: '',
- diseaseInput: '',
- // 快捷标签选中态(仅 UI 提示,值仍以输入框为准)
- heightQuick: '', weightQuick: '', bpQuick: '', glucoseQuick: '', lipidQuick: '',
- // 选项常量
- presetAllergies: PRESET_ALLERGIES,
- presetAvoid: PRESET_AVOID,
- presetFlavor: PRESET_FLAVOR,
- presetCuisine: PRESET_CUISINE,
- presetCooking: PRESET_COOKING,
- presetMustEat: PRESET_MUSTEAT,
- presetGoals: PRESET_GOALS,
- diseaseGroups: DISEASE_GROUPS,
- moduleTitles: {
- basic: '基本', status: '状态', diet: '饮食', disease: '疾病',
- bowel: '排便', sleep: '睡眠', med: '用药', allergy: '过敏'
- },
- currentStatusOptions: ['正常', '哺乳期', '备孕', '怀孕', '住院期', '恢复期', '其他'],
- menstrualOptions: ['规律(28-32天)', '不规律', '已停经', '尚未开始'],
- specialPeriodOptions: ['无', '备孕中', '怀孕', '哺乳期'],
- growthOptions: ['正常(在生长曲线内)', '偏慢', '偏快', '不清楚'],
- pickyEatingOptions: ['无挑食', '轻微挑食', '明显挑食', '严重挑食'],
- osteoporosisOptions: ['无风险', '有风险(曾骨折/骨密度低)', '不清楚'],
- fallRiskOptions: ['近1年无跌倒', '近1年跌倒1次', '近1年跌倒2次及以上'],
- screenTimeOptions: ['<1小时/天', '1-3小时/天', '3-5小时/天', '>5小时/天'],
- teenMoodOptions: ['开朗', '一般', '易怒', '低落'],
- bowelFreqOptions: ['每天1次(正常)', '隔天1次', '每周2-3次', '每周<2次'],
- bowelFormOptions: BOWEL_FORM_OPTIONS,
- exerciseFreqOptions: ['不运动', '每周1-2次', '每周3-4次', '每周5次以上'],
- exerciseDurationOptions: ['30分钟以内', '30-60分钟', '60分钟以上'],
- sleepDurationOptions: ['<6小时', '6-7小时', '7-8小时', '>8小时'],
- bedtimeOptions: ['22:00前', '22:00-24:00', '24:00后'],
- vegFruitOptions: ['<2份', '2-3份', '4份以上'],
- dairyOptions: ['每天', '每周3-5次', '很少', '不喝'],
- sugaryDrinkOptions: ['几乎不喝', '每周1-2次', '每周3-5次', '每天1次以上'],
- allergyOptions: ['无', '反复皮疹', '鼻痒/打喷嚏', '眼痒/流泪', '哮喘样症状', '食物过敏'],
- spiceLabels: ['不辣', '微辣', '中辣', '偏辣', '很辣', '特辣'],
- religiousOptions: [
- { value: 'none', label: '无限制' },
- { value: 'halal', label: '清真' },
- { value: 'vegetarian', label: '素食' },
- { value: 'vegan', label: '纯素' }
- ]
- }
- },
- computed: {
- // 过滤非法 key + 兜底全量 + 去重(保持顺序)
- activeModules: function() {
- var seen = {}
- var list = (this.modules || []).filter(function(m) {
- if (VALID_MODULES.indexOf(m) < 0 || seen[m]) return false
- seen[m] = true
- return true
- })
- if (list.length === 0) list = VALID_MODULES.slice()
- return list
- },
- currentModule: function() {
- return this.activeModules[this.currentStep - 1] || ''
- },
- progressPercent: function() {
- return Math.round((this.currentStep / this.activeModules.length) * 100)
- },
- // 已选疾病名列表(供模板内联 indexOf,避免 :class 带参方法调用)
- selectedDiseaseNames: function() {
- return (this.innerForm.selectedDiseases || []).map(function(d) { return d.name })
- },
- isFemale: function() {
- var g = this.profile && this.profile.gender
- return g === 'female' || g === '女'
- },
- isChild: function() {
- var age = this.profile && this.profile.age
- return age != null && Number(age) > 0 && Number(age) < 12
- },
- isTeenager: function() {
- var age = this.profile && this.profile.age
- return age != null && Number(age) >= 12 && Number(age) < 18
- },
- isElderly: function() {
- var age = this.profile && this.profile.age
- return age != null && Number(age) >= 60
- }
- },
- watch: {
- // 仅在 prop 对象身份变化时重建工作副本(父页换快照/切成员 tab);
- // 不用 deep:v-model 逐键输入会变更共享行对象,deep 会每次按键全量重建
- form: {
- handler: function(v) { this.initFromProps() },
- immediate: true
- },
- diet: {
- handler: function(v) { this.syncDiet(v) },
- immediate: true
- }
- },
- created: function() {
- var now = new Date()
- var y = now.getFullYear()
- var m = now.getMonth() + 1
- var mStr = m < 10 ? '0' + m : '' + m
- this.currentMonth = y + '-' + mStr
- var lastDay = new Date(y, now.getMonth(), 0).getDate()
- this.monthEnd = y + '-' + mStr + '-' + (lastDay < 10 ? '0' + lastDay : '' + lastDay)
- this.initFromProps()
- this.syncDiet(this.diet)
- // 默认展开前 3 组 + 已选疾病所在组
- this.expandedGroups = DISEASE_GROUPS.slice(0, 3).map(function(g) { return g.key })
- var self = this
- ;(this.innerForm.selectedDiseases || []).forEach(function(d) {
- var gk = self.findGroupKey(d.name)
- if (gk && self.expandedGroups.indexOf(gk) < 0) self.expandedGroups.push(gk)
- })
- },
- methods: {
- // ===== 初始化 =====
- initFromProps: function() {
- var f = this.form || {}
- this.innerForm = {
- heightCm: f.heightCm != null ? f.heightCm : '',
- weightKg: f.weightKg != null ? f.weightKg : '',
- bloodPressure: f.bloodPressure != null ? f.bloodPressure : '',
- bloodGlucose: f.bloodGlucose != null ? f.bloodGlucose : '',
- notes: f.notes != null ? f.notes : '',
- lipidCholesterol: f.lipidCholesterol != null ? f.lipidCholesterol : '',
- selectedDiseases: Array.isArray(f.selectedDiseases) ? f.selectedDiseases.slice() : [],
- medRows: Array.isArray(f.medRows) ? f.medRows.slice() : [],
- noMedication: !!f.noMedication
- }
- var ls = f.lifestyle
- var obj = {}
- if (ls && typeof ls === 'object') {
- LIFESTYLE_KEYS.forEach(function(k) {
- obj[k] = ls[k] != null ? (ls[k] instanceof Array ? ls[k].slice() : ls[k]) : (k === 'allergySymptoms' ? [] : '')
- })
- } else {
- LIFESTYLE_KEYS.forEach(function(k) {
- obj[k] = k === 'allergySymptoms' ? [] : ''
- })
- }
- this.lifestyle = obj
- // 旧版 5 档粪便性状文案 → 布里斯托 7 型(存量数据归一,避免回显不匹配)
- if (obj.bowelForm) this.$set(this.lifestyle, 'bowelForm', normalizeBowelForm(obj.bowelForm))
- this._initDiseaseCount = (this.innerForm.selectedDiseases || []).length
- this._initMedCount = (this.innerForm.medRows || []).length
- // 快捷标签回显
- this.matchQuickLabels()
- },
- syncDiet: function(v) {
- var d = v || {}
- this.innerDiet = {
- allergies: Array.isArray(d.allergies) ? d.allergies.slice() : [],
- absoluteAvoid: Array.isArray(d.absoluteAvoid) ? d.absoluteAvoid.slice() : [],
- mustEat: Array.isArray(d.mustEat) ? d.mustEat.slice() : [],
- religiousDiet: d.religiousDiet || 'none',
- spiceLevel: d.spiceLevel != null ? d.spiceLevel : 0,
- flavorPref: Array.isArray(d.flavorPref) ? d.flavorPref.slice() : [],
- cuisinePref: Array.isArray(d.cuisinePref) ? d.cuisinePref.slice() : [],
- cookingMethods: Array.isArray(d.cookingMethods) ? d.cookingMethods.slice() : [],
- healthGoals: Array.isArray(d.healthGoals) ? d.healthGoals.slice() : [],
- noPreference: !!d.noPreference,
- vegetableFruit: d.vegetableFruit || '',
- dairy: d.dairy || '',
- sugaryDrinks: d.sugaryDrinks || ''
- }
- },
- matchQuickLabels: function() {
- this.heightQuick = this.matchQuick('heightCm', 'height')
- this.weightQuick = this.matchQuick('weightKg', 'weight')
- this.bpQuick = this.matchQuick('bloodPressure', 'bloodPressure')
- this.glucoseQuick = this.matchQuick('bloodGlucose', 'bloodGlucose')
- this.lipidQuick = this.matchQuick('lipidCholesterol', 'lipid')
- },
- matchQuick: function(field, quickKey) {
- var QUICK = {
- height: { normal: '170', high: '185', low: '155' },
- weight: { normal: '60', high: '80', low: '45' },
- bloodPressure: { normal: '120/80', high: '140/90', low: '90/60' },
- bloodGlucose: { normal: '5.0', high: '7.0', low: '3.5' },
- lipid: { normal: '4.5', high: '6.2' }
- }
- var map = QUICK[quickKey]
- var val = String(this.innerForm[field] || '')
- var keys = Object.keys(map)
- for (var i = 0; i < keys.length; i++) {
- if (String(map[keys[i]]) === val) return keys[i]
- }
- return ''
- },
- // ===== 步骤导航(前进步时同步快照到父页) =====
- prevStep: function() { if (this.currentStep > 1) this.currentStep-- },
- nextStep: function() {
- if (this.currentStep < this.activeModules.length) {
- this.$emit('update-form', this.formCopy())
- this.$emit('update-diet', this.dietCopy())
- this.currentStep++
- }
- },
- // ===== 提交 =====
- onSubmit: function() {
- if (this.submitting) return
- // 数值校验(与旧页一致)
- if (this.activeModules.indexOf('basic') >= 0) {
- var h = Number(this.innerForm.heightCm)
- var w = Number(this.innerForm.weightKg)
- var g = Number(this.innerForm.bloodGlucose)
- var bp = this.innerForm.bloodPressure
- if (this.innerForm.heightCm && (h < 30 || h > 250)) {
- uni.showToast({ title: '身高应在 30-250 之间', icon: 'none' }); return
- }
- if (this.innerForm.weightKg && (w < 3 || w > 300)) {
- uni.showToast({ title: '体重应在 3-300 之间', icon: 'none' }); return
- }
- if (this.innerForm.bloodGlucose && (g < 1 || g > 30)) {
- uni.showToast({ title: '血糖应在 1-30 之间', icon: 'none' }); return
- }
- if (bp && !/^\d{2,3}\/\d{2,3}$/.test(bp)) {
- uni.showToast({ title: '血压格式应为 120/80', icon: 'none' }); return
- }
- }
- // 空提交拦截
- var formCopy = this.formCopy()
- var dietCopy = this.dietCopy()
- var diff = this.buildLifestyleDiff()
- if (!this.hasAnyContent(formCopy, dietCopy, diff)) {
- uni.showToast({ title: '请至少填写一项', icon: 'none' }); return
- }
- this.$emit('submit', { form: formCopy, diet: dietCopy, lifestyleDiff: diff })
- },
- hasAnyContent: function(form, diet, diff) {
- var keys = ['heightCm', 'weightKg', 'bloodPressure', 'bloodGlucose', 'notes']
- for (var i = 0; i < keys.length; i++) {
- if (form[keys[i]] != null && form[keys[i]] !== '') return true
- }
- if (form.lipidCholesterol != null && form.lipidCholesterol !== '') return true
- if (form.selectedDiseases && form.selectedDiseases.length > 0) return true
- if (form.medRows && form.medRows.length > 0) return true
- if (form.noMedication) return true
- var dkeys = ['allergies', 'absoluteAvoid', 'mustEat', 'flavorPref', 'cuisinePref', 'cookingMethods', 'healthGoals', 'vegetableFruit', 'dairy', 'sugaryDrinks']
- for (var j = 0; j < dkeys.length; j++) {
- var dv = diet[dkeys[j]]
- if (dv instanceof Array) { if (dv.length > 0) return true } else if (dv != null && dv !== '' && dv !== 'none') return true
- }
- if (diet.religiousDiet && diet.religiousDiet !== 'none') return true
- if (diet.spiceLevel && Number(diet.spiceLevel) > 0) return true
- if (diet.noPreference) return true
- return Object.keys(diff).length > 0
- },
- // 结构化 form 副本(不含 lifestyle;JSON 转换由父页负责)
- formCopy: function() {
- var diseases = this.innerForm.selectedDiseases || []
- var meds = this.innerForm.medRows || []
- return {
- heightCm: this.innerForm.heightCm,
- weightKg: this.innerForm.weightKg,
- bloodPressure: this.innerForm.bloodPressure,
- bloodGlucose: this.innerForm.bloodGlucose,
- notes: this.innerForm.notes,
- lipidCholesterol: this.innerForm.lipidCholesterol,
- selectedDiseases: diseases.slice(),
- medRows: meds.slice(),
- noMedication: !!this.innerForm.noMedication,
- // 原本有数据但被用户删空 → 置位,父页下发 '[]' 落库(否则旧值回滚)
- diseasesCleared: this._initDiseaseCount > 0 && diseases.length === 0,
- medsCleared: this._initMedCount > 0 && meds.length === 0
- }
- },
- dietCopy: function() {
- return {
- allergies: this.innerDiet.allergies || [],
- absoluteAvoid: this.innerDiet.absoluteAvoid || [],
- mustEat: this.innerDiet.mustEat || [],
- religiousDiet: this.innerDiet.religiousDiet || 'none',
- spiceLevel: this.innerDiet.spiceLevel || 0,
- flavorPref: this.innerDiet.flavorPref || [],
- cuisinePref: this.innerDiet.cuisinePref || [],
- cookingMethods: this.innerDiet.cookingMethods || [],
- healthGoals: this.innerDiet.healthGoals || [],
- noPreference: !!this.innerDiet.noPreference,
- vegetableFruit: this.innerDiet.vegetableFruit || '',
- dairy: this.innerDiet.dairy || '',
- sugaryDrinks: this.innerDiet.sugaryDrinks || ''
- }
- },
- // lifestyle diff:仅本次修改过的 key(未变化的 key 剔除,交由父页 merge 旧值)
- buildLifestyleDiff: function() {
- var diff = {}
- var orig = (this.form && this.form.lifestyle) || {}
- var self = this
- LIFESTYLE_KEYS.forEach(function(k) {
- var val = self.lifestyle[k]
- if (val == null) return
- var a = JSON.stringify(val)
- var b = JSON.stringify(orig[k] != null ? orig[k] : (k === 'allergySymptoms' ? [] : null))
- if (a === b) return
- if (val instanceof Array) { if (val.length > 0) diff[k] = val.slice() }
- else if (val !== '') diff[k] = val
- // 主动清空:值被删空且该 key 所属模块已启用 → 下发 null 让父页落库清除(未启用模块不清,保护旧数据)
- else if (val === '' && orig[k] && self.activeModules.indexOf(LIFESTYLE_MODULE_MAP[k]) >= 0) {
- diff[k] = null
- }
- })
- return diff
- },
- // ===== lifestyle 工具 =====
- setLifestyle: function(key, val) {
- this.$set(this.lifestyle, key, this.lifestyle[key] === val ? '' : val)
- },
- toggleLifestyleArray: function(key, val) {
- var arr = (this.lifestyle[key] || []).slice()
- var idx = arr.indexOf(val)
- if (idx >= 0) arr.splice(idx, 1); else arr.push(val)
- this.$set(this.lifestyle, key, arr)
- },
- // ===== diet 工具 =====
- setDietValue: function(key, val) {
- this.$set(this.innerDiet, key, val)
- },
- toggleDietChip: function(key, val) {
- var arr = (this.innerDiet[key] || []).slice()
- var idx = arr.indexOf(val)
- if (idx >= 0) arr.splice(idx, 1); else arr.push(val)
- this.$set(this.innerDiet, key, arr)
- this.$set(this.innerDiet, 'noPreference', false)
- },
- setNoPreference: function() {
- this.$set(this.innerDiet, 'noPreference', !this.innerDiet.noPreference)
- },
- onSpiceChanging: function(e) { this.$set(this.innerDiet, 'spiceLevel', e.detail.value) },
- onSpiceChange: function(e) {
- this.$set(this.innerDiet, 'spiceLevel', e.detail.value)
- this.$set(this.innerDiet, 'noPreference', false)
- },
- // ===== 自定义添加 =====
- openCustom: function(field) { this.customField = field; this.customInput = '' },
- addCustom: function() {
- var val = (this.customInput || '').trim()
- if (!val) { this.customField = ''; return }
- var arr = (this.innerDiet[this.customField] || []).slice()
- if (arr.indexOf(val) < 0) {
- arr.push(val)
- this.$set(this.innerDiet, this.customField, arr)
- }
- this.$set(this.innerDiet, 'noPreference', false)
- this.customInput = ''
- this.customField = ''
- },
- // ===== 体征快捷标签 =====
- setHeightQuick: function(key) {
- var v = { normal: '170', high: '185', low: '155' }[key]
- this.$set(this.innerForm, 'heightCm', v); this.heightQuick = key
- },
- setWeightQuick: function(key) {
- var v = { normal: '60', high: '80', low: '45' }[key]
- this.$set(this.innerForm, 'weightKg', v); this.weightQuick = key
- },
- setBpQuick: function(key) {
- var v = { normal: '120/80', high: '140/90', low: '90/60' }[key]
- this.$set(this.innerForm, 'bloodPressure', v); this.bpQuick = key
- },
- setGlucoseQuick: function(key) {
- var v = { normal: '5.0', high: '7.0', low: '3.5' }[key]
- this.$set(this.innerForm, 'bloodGlucose', v); this.glucoseQuick = key
- },
- setLipidQuick: function(key) {
- var v = { normal: '4.5', high: '6.2' }[key]
- this.$set(this.innerForm, 'lipidCholesterol', v); this.lipidQuick = key
- },
- // ===== v-for key 方法(微信小程序 :key 不支持表达式,须用方法调用) =====
- getDiseaseKey: function(gi, ii) { return 'g' + gi + '-' + ii },
- getSelKey: function(si) { return 's' + si },
- getMedKey: function(idx) { return 'm' + idx },
- // ===== 疾病 =====
- findGroupKey: function(name) {
- for (var i = 0; i < DISEASE_GROUPS.length; i++) {
- var items = DISEASE_GROUPS[i].items
- for (var j = 0; j < items.length; j++) {
- if (items[j].name === name) return DISEASE_GROUPS[i].key
- }
- }
- return ''
- },
- toggleGroup: function(key) {
- var idx = this.expandedGroups.indexOf(key)
- if (idx >= 0) this.expandedGroups.splice(idx, 1); else this.expandedGroups.push(key)
- },
- isDiseaseSelected: function(name) {
- return (this.innerForm.selectedDiseases || []).some(function(d) { return d.name === name })
- },
- toggleDisease: function(name) {
- var list = (this.innerForm.selectedDiseases || []).slice()
- var idx = -1
- for (var i = 0; i < list.length; i++) { if (list[i].name === name) { idx = i; break } }
- if (idx >= 0) list.splice(idx, 1); else list.push({ name: name, diagnosedAt: '', custom: false })
- this.$set(this.innerForm, 'selectedDiseases', list)
- },
- addCustomDisease: function() {
- var name = (this.diseaseInput || '').trim()
- if (!name) return
- var list = (this.innerForm.selectedDiseases || []).slice()
- if (!list.some(function(d) { return d.name === name })) {
- list.push({ name: name, diagnosedAt: '', custom: true })
- this.$set(this.innerForm, 'selectedDiseases', list)
- }
- this.diseaseInput = ''
- },
- removeSelected: function(idx) {
- var list = (this.innerForm.selectedDiseases || []).slice()
- list.splice(idx, 1)
- this.$set(this.innerForm, 'selectedDiseases', list)
- },
- onDiagChange: function(idx, e) {
- var list = (this.innerForm.selectedDiseases || []).slice()
- list[idx] = Object.assign({}, list[idx], { diagnosedAt: e.detail.value })
- this.$set(this.innerForm, 'selectedDiseases', list)
- },
- // ===== 用药 =====
- toggleNoMedication: function() {
- this.$set(this.innerForm, 'noMedication', !this.innerForm.noMedication)
- },
- addMed: function() {
- var rows = (this.innerForm.medRows || []).slice()
- rows.push({ name: '', startDate: '', durationText: '', time: '' })
- this.$set(this.innerForm, 'medRows', rows)
- },
- removeMed: function(idx) {
- var rows = (this.innerForm.medRows || []).slice()
- rows.splice(idx, 1)
- this.$set(this.innerForm, 'medRows', rows)
- },
- onMedStartChange: function(idx, e) {
- var start = e.detail.value
- var rows = (this.innerForm.medRows || []).slice()
- rows[idx] = Object.assign({}, rows[idx], { startDate: start, durationText: this.calcDuration(start) })
- this.$set(this.innerForm, 'medRows', rows)
- },
- onForgetClick: function(idx) {
- var self = this
- uni.showModal({
- title: '记不清开始日期',
- editable: true,
- placeholderText: '如:约 3 个月',
- success: function(res) {
- if (res.confirm && res.content) {
- var rows = (self.innerForm.medRows || []).slice()
- rows[idx] = Object.assign({}, rows[idx], { durationText: res.content.trim() })
- self.$set(self.innerForm, 'medRows', rows)
- }
- }
- })
- },
- calcDuration: function(startDate) {
- if (!startDate) return ''
- var parts = startDate.split('-')
- if (parts.length !== 2) return ''
- var sy = parseInt(parts[0], 10)
- var sm = parseInt(parts[1], 10)
- var now = new Date()
- var months = (now.getFullYear() - sy) * 12 + (now.getMonth() + 1 - sm)
- if (months < 1) months = 1
- if (months < 12) return '已服 ' + months + ' 个月'
- return '已服 ' + Math.floor(months / 12) + ' 年'
- }
- }
- }
- </script>
- <style scoped>
- .survey-wrap { min-height: 100vh; background: linear-gradient(180deg, #FFF7ED, #FFF3E6); }
- .progress-bar { padding: 24rpx 32rpx 8rpx; background: #FFF7ED; }
- .progress-track { height: 8rpx; background: #FFE6D2; border-radius: 4rpx; overflow: hidden; }
- .progress-fill { height: 100%; background: #FF8C42; border-radius: 4rpx; transition: width 0.3s; }
- .progress-labels { display: flex; justify-content: space-between; margin-top: 12rpx; }
- .progress-label { font-size: 20rpx; color: #B08968; }
- .progress-label-active { color: #FF8C42; font-weight: 600; }
- .content { padding: 24rpx 32rpx 220rpx; }
- .step-head { display: flex; align-items: flex-start; margin-bottom: 24rpx; }
- .step-icon { font-size: 48rpx; margin-right: 20rpx; }
- .step-head-text { flex: 1; }
- .step-title { display: block; font-size: 36rpx; font-weight: 700; color: #4A3728; }
- .step-sub { display: block; font-size: 22rpx; color: #B08968; margin-top: 8rpx; line-height: 1.5; }
- .card { background: #FFFFFF; border-radius: 24rpx; padding: 28rpx; margin-bottom: 24rpx; box-shadow: 0 4rpx 16rpx rgba(255, 140, 66, 0.08); }
- .card-head { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; }
- .card-title { display: block; font-size: 28rpx; font-weight: 600; color: #4A3728; margin-bottom: 16rpx; }
- .card-head .card-title { margin-bottom: 0; }
- .card-hint { font-size: 22rpx; color: #B08968; margin-bottom: 16rpx; }
- .card-del { font-size: 24rpx; color: #E53E3E; padding: 8rpx 16rpx; }
- .card-value { font-size: 26rpx; color: #4A3728; }
- .group-arrow { font-size: 22rpx; color: #B08968; }
- .card-row { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 0; border-bottom: 1rpx solid #F7EFE6; }
- .card-row:last-child { border-bottom: none; }
- .card-label { font-size: 26rpx; color: #6B5744; }
- .card-input { flex: 1; text-align: right; font-size: 28rpx; color: #4A3728; }
- .picker-val { font-size: 26rpx; color: #FF8C42; }
- .num-input { display: block; width: 100%; background: #FFF3E6; border-radius: 16rpx; padding: 16rpx 20rpx; font-size: 28rpx; color: #4A3728; margin-top: 16rpx; }
- .unit-hint { display: block; font-size: 20rpx; color: #C4A98E; margin-top: 8rpx; }
- .notes-area { width: 100%; min-height: 120rpx; background: #FFF3E6; border-radius: 16rpx; padding: 16rpx 20rpx; font-size: 26rpx; color: #4A3728; margin-top: 16rpx; }
- .quick-chips { display: flex; gap: 12rpx; }
- .quick-chip { padding: 8rpx 20rpx; background: #FFF3E6; color: #6B5744; border-radius: 999rpx; font-size: 20rpx; border: 2rpx solid #FFE6D2; }
- .quick-chip-on { background: #FF8C42; color: #FFFFFF; border-color: #FF8C42; }
- .chip-row-wrap { display: flex; flex-wrap: wrap; gap: 16rpx; }
- .chip-item-wrap { display: flex; align-items: center; gap: 4rpx; padding: 12rpx 28rpx; background: #FFF3E6; color: #6B5744; border-radius: 999rpx; font-size: 24rpx; border: 2rpx solid transparent; }
- .chip-item-wrap.chip-active { background: #FF8C42; color: #FFFFFF; border-color: #FF8C42; font-weight: 600; }
- /* ===== 粪便性状(布里斯托 7 型图片选择) ===== */
- .stool-tip { font-size: 22rpx; color: #B08968; margin-bottom: 16rpx; }
- .stool-grid { display: flex; flex-wrap: wrap; justify-content: space-between; }
- .stool-item {
- width: 48.5%;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 16rpx 12rpx;
- margin-bottom: 16rpx;
- background: #FFFDFA;
- border: 2rpx solid #F0E4D6;
- border-radius: 16rpx;
- }
- .stool-item.stool-active { background: #FFF3E6; border-color: #FF8C42; }
- .stool-img { width: 130rpx; height: 130rpx; }
- .stool-name { font-size: 24rpx; font-weight: 600; color: #4A3728; margin-top: 10rpx; text-align: center; }
- .stool-active .stool-name { color: #FF8C42; }
- .stool-desc { font-size: 20rpx; color: #B08968; margin-top: 4rpx; line-height: 1.4; text-align: center; }
- .risk-badge { color: #E53E3E; font-size: 20rpx; }
- .chip-active .risk-badge { color: #FFE6D2; }
- .custom-row { display: flex; align-items: center; margin-top: 16rpx; gap: 16rpx; }
- .custom-input { flex: 1; background: #FFF3E6; border-radius: 16rpx; padding: 16rpx 20rpx; font-size: 24rpx; color: #4A3728; }
- .custom-confirm { padding: 12rpx 24rpx; background: #FF8C42; color: #FFF; border-radius: 16rpx; font-size: 24rpx; }
- .custom-add { margin-top: 16rpx; font-size: 24rpx; color: #FF8C42; }
- .struct-row { margin-bottom: 24rpx; }
- .struct-row:last-child { margin-bottom: 0; }
- .struct-label { display: block; font-size: 24rpx; color: #6B5744; margin-bottom: 12rpx; }
- .slider-row { display: flex; align-items: center; gap: 24rpx; }
- .sel-row { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 0; border-bottom: 1rpx solid #F7EFE6; }
- .sel-row:last-child { border-bottom: none; }
- .sel-name { flex: 1; font-size: 26rpx; color: #4A3728; }
- .sel-diag { font-size: 24rpx; color: #FF8C42; padding: 8rpx 20rpx; background: #FFF3E6; border-radius: 999rpx; margin: 0 16rpx; }
- .sel-del { font-size: 32rpx; color: #B08968; padding: 0 12rpx; }
- .link-forget { font-size: 24rpx; color: #FF8C42; padding: 8rpx 16rpx; }
- .pref-none-row { margin-bottom: 24rpx; }
- .pref-none-btn { width: 100%; background: #FFF3E6; color: #6B5744; border-radius: 999rpx; font-size: 26rpx; padding: 16rpx; border: 2rpx solid #FFD4B8; }
- .pref-none-btn-active { background: #FF8C42; color: #FFF; border-color: #FF8C42; }
- .add-row { display: flex; align-items: center; justify-content: center; padding: 24rpx; border: 2rpx dashed #FFD4B8; border-radius: 24rpx; color: #FF8C42; font-size: 26rpx; background: #FFFFFF; }
- .add-icon { margin-right: 8rpx; }
- .footer-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom)); background: #FFFFFF; box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06); display: flex; gap: 20rpx; }
- .btn-prev, .btn-next, .btn-done { flex: 1; border-radius: 999rpx; font-size: 28rpx; font-weight: 600; padding: 20rpx 0; background: #FFF3E6; color: #6B5744; line-height: 1.6; }
- .btn-next, .btn-done { background: #FF8C42; color: #FFFFFF; }
- .btn-done[disabled] { opacity: 0.6; }
- </style>
|