report-confirm.vue 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825
  1. <template>
  2. <view class="confirm-container" :style="{ '--font-scale': fontScale }">
  3. <view class="tip-banner">
  4. <text class="tip-icon">✅</text>
  5. <text class="tip-text">请核对以下解析内容,可修改后确认入库</text>
  6. </view>
  7. <!-- 解析中状态(仅 draftId 模式,从上传页跳转后自动解析) -->
  8. <view class="parsing-banner" v-if="parsing">
  9. <text class="parsing-icon">⏳</text>
  10. <text class="parsing-text">正在解析报告,请稍候...</text>
  11. </view>
  12. <!-- 匹配可信度 -->
  13. <view class="confidence-card" v-if="confidenceLevelClass">
  14. <view class="confidence-header">
  15. <text class="confidence-label">匹配可信度</text>
  16. <text class="confidence-badge" :class="'badge-' + confidenceLevelClass">{{ confidenceLevelText }}</text>
  17. </view>
  18. <view class="confidence-bar-track">
  19. <view class="confidence-bar-fill" :class="'fill-' + confidenceLevelClass" :style="'width:' + confidencePercent + '%'"></view>
  20. </view>
  21. <text class="confidence-desc">{{ confidenceDesc }}</text>
  22. </view>
  23. <view class="form-section">
  24. <!-- 基本信息 -->
  25. <view class="card">
  26. <view class="card-title">基本信息</view>
  27. <view class="field-row member-select-row" @click="toggleMemberPanel">
  28. <text class="field-label">被检测人</text>
  29. <text class="field-value" :class="{ 'binding-required': needBind && !selectedMemberObj }">{{ memberDisplayText }}</text>
  30. <text class="member-select-arrow">{{ selectedMemberObj ? '修改 ›' : '选择 ›' }}</text>
  31. </view>
  32. <!-- 被检测人选择面板(内联展开) -->
  33. <view class="member-panel" v-if="showMemberPanel">
  34. <!-- 候选匹配列表(按置信度排序) -->
  35. <view class="candidate-section" v-if="candidates.length > 0">
  36. <text class="candidate-section-hint">以下成员与报告信息匹配,请选择:</text>
  37. <view
  38. class="candidate-item"
  39. v-for="(cand, idx) in candidates"
  40. :key="idx"
  41. :class="{ 'candidate-selected': selectedMemberId === (cand.id || cand.userId) }"
  42. @click="selectAndClose(cand)"
  43. >
  44. <view class="candidate-left">
  45. <text class="candidate-avatar">{{ (cand.nickname || cand.name || '?').substring(0, 1) }}</text>
  46. <view class="candidate-info">
  47. <text class="candidate-name">{{ cand.nickname || cand.name || '未知' }}</text>
  48. <text class="candidate-detail" v-if="cand.gender || cand.age">{{ genderText(cand.gender) }}{{ cand.age ? ' · ' + cand.age + '岁' : '' }}</text>
  49. <text class="candidate-fields" v-if="cand.matchFields">匹配: {{ cand.matchFields }}</text>
  50. </view>
  51. </view>
  52. <view class="candidate-right">
  53. <view class="score-bar-track">
  54. <view class="score-bar-fill" :style="'width:' + (cand.similarityScore * 100) + '%'"></view>
  55. </view>
  56. <text class="score-text">{{ Math.round(cand.similarityScore * 100) }}%</text>
  57. </view>
  58. </view>
  59. </view>
  60. <!-- 全量家庭成员列表(始终显示,候选区已展示的成员去重) -->
  61. <view class="member-list" v-if="familyMembers.length > 0">
  62. <text class="member-list-hint" v-if="candidates.length > 0">全部成员({{ otherMembers.length }} 人)</text>
  63. <view
  64. class="member-item"
  65. v-for="(member, mIdx) in otherMembers"
  66. :key="mIdx"
  67. :class="{ 'member-selected': selectedMemberId === (member.userId || member.id) }"
  68. @click="selectAndClose(member)"
  69. >
  70. <text class="member-avatar">{{ (member.nickname || '?').substring(0, 1) }}</text>
  71. <text class="member-name">{{ member.nickname || '未知' }}</text>
  72. </view>
  73. </view>
  74. <!-- 新建成员入口 -->
  75. <view class="create-member-entry" @click="toggleAutoCreate">
  76. <text class="create-member-icon">+</text>
  77. <text class="create-member-text">{{ showAutoCreateForm ? '收起新建表单' : '不在列表中?新建家庭成员' }}</text>
  78. </view>
  79. <!-- 新建成员表单(面板内嵌) -->
  80. <view class="auto-create-inline" v-if="showAutoCreateForm">
  81. <view class="form-item">
  82. <text class="form-item-label">姓名</text>
  83. <input class="form-item-input" v-model="newMember.name" placeholder="请输入姓名" />
  84. </view>
  85. <view class="form-item">
  86. <text class="form-item-label">性别</text>
  87. <view class="gender-selector">
  88. <text class="gender-option" :class="{ 'gender-selected': newMember.gender === 'male' }" @click="newMember.gender = 'male'">男</text>
  89. <text class="gender-option" :class="{ 'gender-selected': newMember.gender === 'female' }" @click="newMember.gender = 'female'">女</text>
  90. </view>
  91. </view>
  92. <view class="form-item">
  93. <text class="form-item-label">年龄</text>
  94. <input class="form-item-input" type="number" v-model="newMember.age" placeholder="请输入年龄" />
  95. </view>
  96. <view class="btn-create-submit" :class="{ 'btn-disabled': creatingMember }" @click="doAutoCreate">
  97. <text v-if="creatingMember">创建中...</text>
  98. <text v-else>确认创建</text>
  99. </view>
  100. </view>
  101. </view>
  102. <view class="field-row" v-if="extractedGender">
  103. <text class="field-label">性别</text>
  104. <text class="field-value">{{ extractedGender === 'male' ? '男' : extractedGender === 'female' ? '女' : extractedGender }}</text>
  105. </view>
  106. <view class="field-row" v-if="extractedAge">
  107. <text class="field-label">年龄</text>
  108. <text class="field-value">{{ extractedAge }}</text>
  109. </view>
  110. <view class="field-row" v-if="payload.summary && payload.type !== 'tongue'">
  111. <text class="field-label">综合评分</text>
  112. <input class="field-input" type="number" v-model="payload.summary.overallScore" />
  113. </view>
  114. <view class="field-row" v-if="payload.summary && payload.type !== 'tongue'">
  115. <text class="field-label">报告日期</text>
  116. <input class="field-input" type="text" v-model="payload.summary.reportDate" placeholder="如: 2025-06-15" />
  117. </view>
  118. <view class="field-row" v-if="payload.summary && payload.type !== 'tongue'">
  119. <text class="field-label">报告摘要</text>
  120. <textarea class="field-textarea" v-model="payload.summary.summary" maxlength="500" />
  121. </view>
  122. </view>
  123. <!-- 舌诊指标(只读展示) -->
  124. <view class="card" v-if="payload.type === 'tongue' && payload.indicators && payload.indicators.length">
  125. <view class="card-title">舌象指标</view>
  126. <view class="tongue-indicator-grid">
  127. <view class="tongue-ind-item" v-for="(ind, idx) in payload.indicators" :key="idx">
  128. <text class="tongue-ind-label">{{ indicatorLabel(ind.code) }}</text>
  129. <text class="tongue-ind-value">{{ ind.value }}</text>
  130. </view>
  131. </view>
  132. <view class="tongue-summary" v-if="payload.summary && payload.summary.overallAssessment">
  133. <text class="tongue-summary-title">综合评估</text>
  134. <text class="tongue-summary-text">{{ payload.summary.overallAssessment }}</text>
  135. </view>
  136. </view>
  137. <!-- 健康指标(舌诊时隐藏,使用上方舌象指标卡片) -->
  138. <view class="card" v-if="payload.type !== 'tongue' && payload.indicators && payload.indicators.length">
  139. <view class="card-title">
  140. <text>健康指标</text>
  141. <text class="title-count">共 {{ payload.indicators.length }} 项</text>
  142. <text class="title-abnormal" v-if="totalAbnormalCount > 0">,{{ totalAbnormalCount }} 项异常</text>
  143. <view class="card-title-actions">
  144. <text class="expand-all-btn" @click="toggleAllGroups(true)">全部展开</text>
  145. <text class="expand-all-btn" @click="toggleAllGroups(false)">全部折叠</text>
  146. </view>
  147. </view>
  148. <!-- 按指标类型分组折叠 -->
  149. <view class="indicator-group" v-for="(group, gIdx) in indicatorGroups" :key="gIdx">
  150. <view class="indicator-group-header" @click="toggleGroup(gIdx)">
  151. <view class="indicator-group-header-left">
  152. <text class="indicator-group-arrow" :class="{ 'arrow-open': groupExpandList[gIdx] }">›</text>
  153. <text class="indicator-group-name">{{ group.category }}</text>
  154. </view>
  155. <view class="indicator-group-header-right">
  156. <text class="indicator-group-abnormal" v-if="group.abnormalCount > 0">{{ group.abnormalCount }} 项异常</text>
  157. <text class="indicator-group-count">{{ group.items.length }} 项</text>
  158. </view>
  159. </view>
  160. <view class="indicator-group-body" v-if="groupExpandList[gIdx]">
  161. <view class="indicator-item" v-for="(ind, idx) in group.items" :key="idx">
  162. <view class="ind-header">
  163. <text class="ind-help-btn" @click.stop="showHelp(ind)">?</text>
  164. <input class="ind-name" :class="'ind-name-' + _cssClass(ind.status)" v-model="ind.indicatorName" />
  165. <input class="ind-value" :class="'ind-value-' + _cssClass(ind.status)" v-model="ind.indicatorValue" />
  166. </view>
  167. <view class="ind-detail" v-if="ind.unit || ind.refRange">
  168. <text class="ind-ref" v-if="ind.refRange">参考: {{ ind.refRange }}</text>
  169. <text class="ind-unit" v-if="ind.unit">{{ ind.unit }}</text>
  170. </view>
  171. <view class="ind-symptoms" v-if="ind.symptoms">
  172. <text class="ind-symptom-label">症状:</text>
  173. <input class="ind-symptom-input" v-model="ind.symptoms" />
  174. </view>
  175. </view>
  176. </view>
  177. </view>
  178. </view>
  179. <!-- 指标帮助弹窗 -->
  180. <view class="help-overlay" v-if="showHelpPopup" @click="closeHelp">
  181. <view class="help-popup" @click.stop>
  182. <view class="help-popup-header">
  183. <text class="help-popup-title">{{ helpData.name }}</text>
  184. <text class="help-popup-close" @click="closeHelp">✕</text>
  185. </view>
  186. <view class="help-popup-body">
  187. <view class="help-row">
  188. <text class="help-label">当前值</text>
  189. <text class="help-value">{{ helpData.value }}</text>
  190. </view>
  191. <view class="help-row" v-if="helpData.refRange">
  192. <text class="help-label">参考范围</text>
  193. <text class="help-value">{{ helpData.refRange }}</text>
  194. </view>
  195. <view class="help-row">
  196. <text class="help-label">状态</text>
  197. <text class="help-value" :class="'help-status-' + _cssClass(helpData.status)">{{ statusText(helpData.status) }}</text>
  198. </view>
  199. <view class="help-desc" v-if="helpData.description">
  200. <text class="help-desc-label">说明</text>
  201. <text class="help-desc-text">{{ helpData.description }}</text>
  202. </view>
  203. <view class="help-desc" v-else>
  204. <text class="help-desc-label">说明</text>
  205. <text class="help-desc-text">{{ getDefaultDescription(helpData.name, helpData.status) }}</text>
  206. </view>
  207. </view>
  208. </view>
  209. </view>
  210. <!-- 肠道菌群 -->
  211. <view class="card" v-if="payload.gutFlora && payload.gutFlora.length">
  212. <view class="card-title">肠道菌群 ({{ payload.gutFlora.length }})</view>
  213. <view class="flora-item" v-for="(flora, idx) in payload.gutFlora" :key="idx">
  214. <input class="flora-name" v-model="payload.gutFlora[idx].name" />
  215. <input class="flora-abundance" type="digit" v-model="payload.gutFlora[idx].abundance" />
  216. <view class="status-tag" :class="'status-' + _cssClass(flora.status)">
  217. {{ statusText(flora.status) }}
  218. </view>
  219. </view>
  220. </view>
  221. <!-- 疾病风险 -->
  222. <view class="card" v-if="payload.diseaseRisks && payload.diseaseRisks.length">
  223. <view class="card-title">疾病风险 ({{ payload.diseaseRisks.length }})</view>
  224. <view class="risk-item" v-for="(risk, idx) in payload.diseaseRisks" :key="idx">
  225. <input class="risk-name" v-model="payload.diseaseRisks[idx].diseaseName" />
  226. <view class="risk-level" :class="'risk-' + (risk.riskLevel || 'low')">
  227. {{ riskLevelText(risk.riskLevel) }}
  228. </view>
  229. </view>
  230. </view>
  231. <!-- 饮食推荐 -->
  232. <view class="card" v-if="payload.foods && payload.foods.length">
  233. <view class="card-title">饮食推荐 ({{ payload.foods.length }})</view>
  234. <view class="food-item" v-for="(food, idx) in payload.foods" :key="idx">
  235. <input class="food-name" v-model="payload.foods[idx].foodName" />
  236. <input class="food-category" v-model="payload.foods[idx].category" />
  237. </view>
  238. </view>
  239. <!-- 益生菌种 -->
  240. <view class="card" v-if="payload.probioticSpecies && payload.probioticSpecies.length">
  241. <view class="card-title">益生菌种 ({{ payload.probioticSpecies.length }})</view>
  242. <view class="flora-item" v-for="(flora, idx) in payload.probioticSpecies" :key="idx">
  243. <input class="flora-name" v-model="payload.probioticSpecies[idx].name" />
  244. <input class="flora-abundance" type="digit" v-model="payload.probioticSpecies[idx].abundance" />
  245. <view class="status-tag" :class="'status-' + _cssClass(flora.status)">
  246. {{ statusText(flora.status) }}
  247. </view>
  248. </view>
  249. </view>
  250. <!-- Taxonomy 分类(纲/目/科/属/种) -->
  251. <view class="card" v-if="hasTaxonomyData()">
  252. <view class="card-title">菌群分类组成 ({{ taxonomyTotalCount }})</view>
  253. <view class="taxonomy-group" v-if="payload.taxonomyClass && payload.taxonomyClass.length">
  254. <text class="taxonomy-group-label">菌纲 (Class)</text>
  255. <view class="flora-item" v-for="(flora, idx) in payload.taxonomyClass" :key="getTaxonomyKey('c', idx)">
  256. <input class="flora-name" v-model="payload.taxonomyClass[idx].name" />
  257. <input class="flora-abundance" type="digit" v-model="payload.taxonomyClass[idx].abundance" />
  258. </view>
  259. </view>
  260. <view class="taxonomy-group" v-if="payload.taxonomyOrder && payload.taxonomyOrder.length">
  261. <text class="taxonomy-group-label">菌目 (Order)</text>
  262. <view class="flora-item" v-for="(flora, idx) in payload.taxonomyOrder" :key="getTaxonomyKey('o', idx)">
  263. <input class="flora-name" v-model="payload.taxonomyOrder[idx].name" />
  264. <input class="flora-abundance" type="digit" v-model="payload.taxonomyOrder[idx].abundance" />
  265. </view>
  266. </view>
  267. <view class="taxonomy-group" v-if="payload.taxonomyFamily && payload.taxonomyFamily.length">
  268. <text class="taxonomy-group-label">菌科 (Family)</text>
  269. <view class="flora-item" v-for="(flora, idx) in payload.taxonomyFamily" :key="getTaxonomyKey('f', idx)">
  270. <input class="flora-name" v-model="payload.taxonomyFamily[idx].name" />
  271. <input class="flora-abundance" type="digit" v-model="payload.taxonomyFamily[idx].abundance" />
  272. </view>
  273. </view>
  274. <view class="taxonomy-group" v-if="payload.taxonomyGenus && payload.taxonomyGenus.length">
  275. <text class="taxonomy-group-label">菌属 (Genus)</text>
  276. <view class="flora-item" v-for="(flora, idx) in payload.taxonomyGenus" :key="getTaxonomyKey('g', idx)">
  277. <input class="flora-name" v-model="payload.taxonomyGenus[idx].name" />
  278. <input class="flora-abundance" type="digit" v-model="payload.taxonomyGenus[idx].abundance" />
  279. </view>
  280. </view>
  281. <view class="taxonomy-group" v-if="payload.taxonomySpecies && payload.taxonomySpecies.length">
  282. <text class="taxonomy-group-label">菌种 (Species)</text>
  283. <view class="flora-item" v-for="(flora, idx) in payload.taxonomySpecies" :key="getTaxonomyKey('s', idx)">
  284. <input class="flora-name" v-model="payload.taxonomySpecies[idx].name" />
  285. <input class="flora-abundance" type="digit" v-model="payload.taxonomySpecies[idx].abundance" />
  286. </view>
  287. </view>
  288. </view>
  289. <!-- 病原菌 -->
  290. <view class="card" v-if="hasPathogenData()">
  291. <view class="card-title">病原菌 ({{ pathogenTotalCount }})</view>
  292. <view class="pathogen-group" v-if="payload.pathogenGenus && payload.pathogenGenus.length">
  293. <text class="pathogen-group-label">病原菌属</text>
  294. <view class="flora-item" v-for="(flora, idx) in payload.pathogenGenus" :key="getTaxonomyKey('pg', idx)">
  295. <input class="flora-name" v-model="payload.pathogenGenus[idx].name" />
  296. <input class="flora-abundance" type="digit" v-model="payload.pathogenGenus[idx].abundance" />
  297. <view class="status-tag" :class="'status-' + _cssClass(flora.status)">
  298. {{ statusText(flora.status) }}
  299. </view>
  300. </view>
  301. </view>
  302. <view class="pathogen-group" v-if="payload.pathogenDetection && payload.pathogenDetection.length">
  303. <text class="pathogen-group-label">病原菌检出</text>
  304. <view class="flora-item" v-for="(flora, idx) in payload.pathogenDetection" :key="getTaxonomyKey('pd', idx)">
  305. <input class="flora-name" v-model="payload.pathogenDetection[idx].name" />
  306. <input class="flora-abundance" type="digit" v-model="payload.pathogenDetection[idx].abundance" />
  307. <view class="status-tag" :class="'status-' + _cssClass(flora.status)">
  308. {{ statusText(flora.status) }}
  309. </view>
  310. </view>
  311. </view>
  312. </view>
  313. <!-- 操作按钮 -->
  314. <view class="action-row">
  315. <view class="btn-discard" @click="doDiscard">
  316. <text>放弃</text>
  317. </view>
  318. <view class="btn-confirm" :class="{ 'btn-disabled': !canConfirm }" @click="doConfirm">
  319. <text v-if="confirming">确认中...</text>
  320. <text v-else>确认入库</text>
  321. </view>
  322. </view>
  323. </view>
  324. <!-- 报告反馈入口(解析完成、非解析中) -->
  325. <view class="feedback-entry" v-if="!parsing && draftId" @tap="openFeedback">
  326. <text class="feedback-entry-icon">💬</text>
  327. <text class="feedback-entry-text">对报告有疑问或建议</text>
  328. <text class="feedback-entry-arrow">›</text>
  329. </view>
  330. <feedback-popup :visible="feedbackVisible" :draft-id="draftId" @close="closeFeedback" />
  331. <view class="bottom-spacer"></view>
  332. </view>
  333. </template>
  334. <script>
  335. import { confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, parseReportDraft } from '../../utils/api.js'
  336. import { addFamilyMember, getFamilyMemberList } from '../../utils/api.js'
  337. import FeedbackPopup from '@/pages/health/components/feedback-popup.vue'
  338. import fontScaleMixin from '@/mixins/font-scale.js'
  339. export default {
  340. mixins: [fontScaleMixin],
  341. components: { FeedbackPopup },
  342. data() {
  343. return {
  344. draftId: null,
  345. memberId: null,
  346. familyId: null,
  347. payload: {
  348. summary: {},
  349. indicators: [],
  350. gutFlora: [],
  351. probioticSpecies: [],
  352. taxonomyClass: [],
  353. taxonomyOrder: [],
  354. taxonomyFamily: [],
  355. taxonomyGenus: [],
  356. taxonomySpecies: [],
  357. pathogenGenus: [],
  358. pathogenDetection: [],
  359. foods: [],
  360. diseaseRisks: []
  361. },
  362. extractedName: '',
  363. extractedGender: '',
  364. extractedAge: '',
  365. feedbackVisible: false,
  366. matchedMemberId: null,
  367. needBind: false,
  368. selectedMemberId: null,
  369. familyMembers: [],
  370. confirming: false,
  371. // 仅 draftId 模式(从上传页跳转)下正在解析
  372. parsing: false,
  373. // Phase 2: 匹配可信度与候选
  374. matchConfidence: null,
  375. matchLabel: '',
  376. candidates: [],
  377. // 被检测人选择面板
  378. showMemberPanel: false,
  379. selectedMemberObj: null,
  380. // 新建成员
  381. showAutoCreateForm: false,
  382. newMember: {
  383. name: '',
  384. gender: '',
  385. age: ''
  386. },
  387. creatingMember: false,
  388. createdMemberId: null,
  389. // 指标分组折叠状态(按分组下标,默认全部折叠)
  390. groupExpandList: [],
  391. // 指标帮助弹窗
  392. showHelpPopup: false,
  393. helpData: { name: '', value: '', refRange: '', status: '', description: '' }
  394. }
  395. },
  396. computed: {
  397. canConfirm() {
  398. if (this.parsing) return false
  399. if (this.needBind && !this.selectedMemberId && !this.createdMemberId) return false
  400. // 舌诊走 recordId,不走 draft
  401. if (this.payload.type === 'tongue') {
  402. return !!this.payload.recordId && !this.confirming
  403. }
  404. return this.draftId && !this.confirming
  405. },
  406. confidenceLevelClass() {
  407. if (!this.matchConfidence && this.matchConfidence !== 0) return ''
  408. var val = parseFloat(this.matchConfidence)
  409. if (this.matchLabel === 'exact' || val >= 0.8) return 'high'
  410. if (this.matchLabel === 'fuzzy' || val >= 0.5) return 'medium'
  411. if (this.matchLabel === 'profile_match' || val >= 0.2) return 'low'
  412. return 'none'
  413. },
  414. confidenceLevelText() {
  415. var map = { high: '高', medium: '中', low: '低', none: '未匹配' }
  416. return map[this.confidenceLevelClass] || ''
  417. },
  418. confidencePercent() {
  419. if (!this.matchConfidence && this.matchConfidence !== 0) return 0
  420. return Math.round(parseFloat(this.matchConfidence) * 100)
  421. },
  422. confidenceDesc() {
  423. if (this.matchLabel === 'exact') return '根据姓名、性别、年龄自动精确匹配到家庭成员'
  424. if (this.matchLabel === 'fuzzy') return '部分字段匹配,请确认是否为以下家庭成员'
  425. if (this.matchLabel === 'profile_match') return '根据档案信息推测匹配,请手动确认'
  426. if (this.matchLabel === 'no_match') return '无法匹配到现有家庭成员,可新建成员'
  427. return ''
  428. },
  429. memberDisplayText() {
  430. var m = this.selectedMemberObj
  431. if (m && (m.nickname || m.name)) return m.nickname || m.name
  432. if (this.needBind) return '需手动绑定'
  433. return this.extractedName || '未识别'
  434. },
  435. /** 全量成员减去候选区已展示的成员,避免重复 */
  436. otherMembers() {
  437. var candIds = {}
  438. for (var i = 0; i < this.candidates.length; i++) {
  439. var c = this.candidates[i]
  440. var cid = c.id || c.userId
  441. if (cid !== null && cid !== undefined) candIds[cid] = true
  442. }
  443. var list = []
  444. for (var j = 0; j < this.familyMembers.length; j++) {
  445. var m = this.familyMembers[j]
  446. var mid = m.userId || m.id
  447. if (mid !== null && mid !== undefined && candIds[mid]) continue
  448. list.push(m)
  449. }
  450. return list
  451. },
  452. // 指标按类型(category)分组,保持解析顺序,统计需处理(非正常)数量
  453. indicatorGroups() {
  454. var groups = []
  455. var orderMap = {}
  456. var list = (this.payload && this.payload.indicators) || []
  457. for (var i = 0; i < list.length; i++) {
  458. var ind = list[i]
  459. var cat = ind.category || '其他指标'
  460. if (!orderMap[cat]) {
  461. orderMap[cat] = groups.length
  462. groups.push({ category: cat, items: [], needCount: 0, abnormalCount: 0 })
  463. }
  464. var group = groups[orderMap[cat]]
  465. group.items.push(ind)
  466. if (this.isAbnormalStatus(ind.status)) {
  467. group.needCount++
  468. group.abnormalCount++
  469. }
  470. }
  471. return groups
  472. },
  473. totalAbnormalCount() {
  474. var list = (this.payload && this.payload.indicators) || []
  475. var count = 0
  476. for (var i = 0; i < list.length; i++) {
  477. if (this.isAbnormalStatus(list[i].status)) count++
  478. }
  479. return count
  480. },
  481. taxonomyTotalCount() {
  482. var total = 0
  483. total += (this.payload.taxonomyClass || []).length
  484. total += (this.payload.taxonomyOrder || []).length
  485. total += (this.payload.taxonomyFamily || []).length
  486. total += (this.payload.taxonomyGenus || []).length
  487. total += (this.payload.taxonomySpecies || []).length
  488. return total
  489. },
  490. pathogenTotalCount() {
  491. return (this.payload.pathogenGenus || []).length +
  492. (this.payload.pathogenDetection || []).length
  493. }
  494. },
  495. onLoad: function(options) {
  496. this.draftId = options.draftId ? parseInt(options.draftId) : null
  497. if (this.draftId) {
  498. // gut_flora 走个性化详情页,其他类型走通用详情页
  499. var gotoUrl = '/pages/health/gut-flora-detail?draftId=' + this.draftId
  500. uni.redirectTo({ url: gotoUrl })
  501. return
  502. }
  503. this.memberId = options.memberId ? parseInt(options.memberId) : null
  504. this.familyId = options.familyId ? parseInt(options.familyId) : null
  505. // 仅 draftId 模式(从上传页自动跳转):无 payload 参数 → 调用后端按草稿解析并自动填充
  506. if (this.draftId && !options.payload) {
  507. this.parseByDraftId()
  508. return
  509. }
  510. var payloadStr = decodeURIComponent(options.payload || '{}')
  511. try {
  512. var parsed = typeof payloadStr === 'string' ? JSON.parse(payloadStr) : payloadStr
  513. this.payload = Object.assign({
  514. summary: {},
  515. indicators: [],
  516. gutFlora: [],
  517. probioticSpecies: [],
  518. taxonomyClass: [],
  519. taxonomyOrder: [],
  520. taxonomyFamily: [],
  521. taxonomyGenus: [],
  522. taxonomySpecies: [],
  523. pathogenGenus: [],
  524. pathogenDetection: [],
  525. foods: [],
  526. diseaseRisks: []
  527. }, parsed)
  528. } catch (e) {
  529. console.log('解析payload失败', e)
  530. }
  531. this.extractedName = options.extractedName || ''
  532. this.extractedGender = options.extractedGender || ''
  533. this.extractedAge = options.extractedAge || ''
  534. this.matchedMemberId = options.matchedMemberId ? parseInt(options.matchedMemberId) : null
  535. // 从解析结果填充报告日期
  536. var extractedReportDate = options.extractedReportDate || ''
  537. if (extractedReportDate && this.payload.summary) {
  538. this.payload.summary.reportDate = extractedReportDate
  539. }
  540. this.needBind = options.needBind === 'true' || options.needBind === '1'
  541. this.matchConfidence = options.matchConfidence ? parseFloat(options.matchConfidence) : null
  542. this.matchLabel = decodeURIComponent(options.matchLabel || '')
  543. // 解析候选成员列表
  544. var candidatesStr = decodeURIComponent(options.candidates || '')
  545. if (candidatesStr) {
  546. try {
  547. this.candidates = JSON.parse(candidatesStr)
  548. } catch (e) {
  549. this.candidates = []
  550. }
  551. }
  552. // 始终加载家庭成员(面板始终可点击,needBind=false 也允许改选)
  553. this.loadFamilyMembers()
  554. if (this.needBind) {
  555. // 高置信度且已匹配 → 直接选中
  556. if (this.matchedMemberId && this.matchConfidence >= 0.8) {
  557. this.selectedMemberId = this.matchedMemberId
  558. }
  559. }
  560. },
  561. methods: {
  562. // ===== 报告反馈(原「快速分析」入口已合并,反馈由 AI 解答) =====
  563. openFeedback: function() {
  564. this.feedbackVisible = true
  565. },
  566. closeFeedback: function() {
  567. this.feedbackVisible = false
  568. },
  569. // 仅 draftId 模式:调用后端按草稿解析,成功后自动填充页面
  570. parseByDraftId() {
  571. var self = this
  572. self.parsing = true
  573. parseReportDraft(self.draftId).then(function(res) {
  574. var detail = res && res.data
  575. if (!detail) {
  576. self.parsing = false
  577. uni.showToast({ title: (res && res.message) || '解析失败,请重试', icon: 'none' })
  578. return
  579. }
  580. self.applyParseResult(detail)
  581. }).catch(function(err) {
  582. self.parsing = false
  583. var msg = (err && err.message) || '解析失败,请重试'
  584. uni.showToast({ title: msg, icon: 'none' })
  585. })
  586. },
  587. // 将后端解析结果应用到页面
  588. applyParseResult(detail) {
  589. this.parsing = false
  590. if (detail.payload) {
  591. this.payload = Object.assign({
  592. summary: {},
  593. indicators: [],
  594. gutFlora: [],
  595. probioticSpecies: [],
  596. taxonomyClass: [],
  597. taxonomyOrder: [],
  598. taxonomyFamily: [],
  599. taxonomyGenus: [],
  600. taxonomySpecies: [],
  601. pathogenGenus: [],
  602. pathogenDetection: [],
  603. foods: [],
  604. diseaseRisks: []
  605. }, detail.payload)
  606. }
  607. this.extractedName = detail.extractedName || ''
  608. this.extractedGender = detail.extractedGender || ''
  609. this.extractedAge = detail.extractedAge || ''
  610. this.matchedMemberId = detail.matchedMemberId ? parseInt(detail.matchedMemberId) : null
  611. this.needBind = !!detail.needBind
  612. this.matchConfidence = detail.confidence !== undefined && detail.confidence !== null ? parseFloat(detail.confidence) : null
  613. this.matchLabel = detail.matchLabel || ''
  614. this.candidates = detail.candidates || []
  615. // 始终加载家庭成员
  616. this.loadFamilyMembers()
  617. // 高置信度且已匹配 → 直接选中
  618. if (this.needBind && this.matchedMemberId && this.matchConfidence >= 0.8) {
  619. this.selectedMemberId = this.matchedMemberId
  620. }
  621. },
  622. loadFamilyMembers() {
  623. var self = this
  624. // 从后端获取真实家庭成员(家长 + 孩子),storage 中并无该数据
  625. getFamilyMemberList({ visibleOnly: true }).then(function(res) {
  626. var list = (res && res.data) || []
  627. var parents = list.filter(function(m) { return m.source === 'parent' }).map(function(p) {
  628. return { id: p.id, nickname: p.nickname, gender: p.gender, role: 'parent' }
  629. })
  630. var children = list.filter(function(m) { return m.source !== 'parent' }).map(function(c) {
  631. return { id: c.id, nickname: c.nickname, gender: c.gender, age: c.age, role: 'child' }
  632. })
  633. self.familyMembers = parents.concat(children)
  634. self.syncSelectedMemberObj()
  635. }).catch(function(e) {
  636. console.log('加载家庭成员失败', e)
  637. self.familyMembers = []
  638. })
  639. },
  640. toggleMemberPanel() {
  641. this.showMemberPanel = !this.showMemberPanel
  642. if (!this.showMemberPanel) {
  643. this.showAutoCreateForm = false
  644. }
  645. },
  646. selectAndClose(member) {
  647. var memberId = member.id || member.userId
  648. this.selectedMemberId = memberId
  649. this.selectedMemberObj = member
  650. this.extractedName = member.nickname || member.name || this.extractedName
  651. this.extractedGender = member.gender || this.extractedGender
  652. this.extractedAge = member.age || this.extractedAge
  653. this.showAutoCreateForm = false
  654. this.showMemberPanel = false
  655. },
  656. syncSelectedMemberObj() {
  657. if (!this.selectedMemberId) {
  658. this.selectedMemberObj = null
  659. return
  660. }
  661. var all = this.familyMembers.concat(this.candidates)
  662. for (var i = 0; i < all.length; i++) {
  663. var m = all[i]
  664. if ((m.id || m.userId) === this.selectedMemberId) {
  665. this.selectedMemberObj = m
  666. return
  667. }
  668. }
  669. },
  670. // === Phase 2: 候选匹配与新建成员 ===
  671. genderText(gender) {
  672. if (gender === 'male') return '男'
  673. if (gender === 'female') return '女'
  674. return gender || ''
  675. },
  676. toggleAutoCreate() {
  677. this.showAutoCreateForm = !this.showAutoCreateForm
  678. if (this.showAutoCreateForm) {
  679. this.newMember.name = this.extractedName || ''
  680. this.newMember.gender = this.extractedGender || ''
  681. this.newMember.age = this.extractedAge || ''
  682. this.selectedMemberId = null
  683. }
  684. },
  685. doAutoCreate() {
  686. var self = this
  687. if (!this.newMember.name) {
  688. uni.showToast({ title: '请输入成员姓名', icon: 'none' })
  689. return
  690. }
  691. self.creatingMember = true
  692. uni.showLoading({ title: '创建成员...' })
  693. addFamilyMember({
  694. nickname: self.newMember.name,
  695. gender: self.newMember.gender || '',
  696. age: self.newMember.age ? parseInt(self.newMember.age) : null
  697. }).then(function(res) {
  698. uni.hideLoading()
  699. if (res.code === 200) {
  700. var memberId = res.data && (res.data.userId || res.data.id)
  701. if (memberId) {
  702. self.createdMemberId = memberId
  703. self.selectedMemberId = memberId
  704. self.selectedMemberObj = {
  705. id: memberId,
  706. nickname: self.newMember.name,
  707. gender: self.newMember.gender,
  708. age: self.newMember.age
  709. }
  710. self.extractedName = self.newMember.name
  711. self.extractedGender = self.newMember.gender
  712. self.extractedAge = self.newMember.age
  713. self.showAutoCreateForm = false
  714. self.showMemberPanel = false
  715. uni.showToast({ title: '创建成功', icon: 'success' })
  716. }
  717. } else {
  718. uni.showToast({ title: res.message || '创建失败', icon: 'none' })
  719. }
  720. }).catch(function(e) {
  721. uni.hideLoading()
  722. uni.showToast({ title: '创建失败', icon: 'none' })
  723. }).finally(function() {
  724. self.creatingMember = false
  725. })
  726. },
  727. toggleGroup(gIdx) {
  728. this.$set(this.groupExpandList, gIdx, !this.groupExpandList[gIdx])
  729. },
  730. toggleAllGroups(expand) {
  731. var arr = []
  732. for (var i = 0; i < this.indicatorGroups.length; i++) {
  733. arr.push(expand)
  734. }
  735. this.groupExpandList = arr
  736. },
  737. _cssClass(status) {
  738. var map = { 'high': 'high', 'low': 'low', 'abnormal': 'abnormal', 'normal': 'normal', '缺乏': 'deficient', '偏低': 'low', '不足': 'insufficient', '正常': 'normal', '注意': 'warning', '偏高': 'high', '异常': 'abnormal', '高风险': 'high-risk', '低风险': 'low-risk', '未检出': 'not-detected' }
  739. return map[status] || 'normal'
  740. },
  741. statusText(status) {
  742. if (status === 'high' || status === '偏高') return '偏高'
  743. if (status === 'low' || status === '偏低') return '偏低'
  744. if (status === 'abnormal' || status === '异常') return '异常'
  745. if (status === '缺乏') return '缺乏'
  746. if (status === '不足') return '不足'
  747. if (status === '正常') return '正常'
  748. if (status === '注意') return '注意'
  749. if (status === '高风险') return '高风险'
  750. if (status === '低风险') return '低风险'
  751. if (status === '未检出') return '未检出'
  752. return '正常'
  753. },
  754. // 与 _cssClass 保持同一白名单:仅明确异常/需关注的状态计入异常数,
  755. // 未知状态文本(解析器自由提取)一律视为正常,避免"样式正常却计入异常"的矛盾。
  756. isAbnormalStatus(status) {
  757. if (!status) return false
  758. var abnormalMap = { 'high': 1, 'low': 1, 'abnormal': 1, '偏高': 1, '偏低': 1, '异常': 1, '缺乏': 1, '不足': 1, '注意': 1, '高风险': 1 }
  759. return !!abnormalMap[String(status).trim()]
  760. },
  761. showHelp(ind) {
  762. this.helpData = {
  763. name: ind.indicatorName || ind.name || '',
  764. value: ind.indicatorValue || ind.value || '',
  765. refRange: ind.refRange || ind.normalRange || '',
  766. status: ind.status || 'normal',
  767. description: ind.description || ind.symptoms || ''
  768. }
  769. this.showHelpPopup = true
  770. },
  771. closeHelp() {
  772. this.showHelpPopup = false
  773. },
  774. getDefaultDescription(name, status) {
  775. if (!status || status === 'normal') return '该指标处于正常范围内,无需特别关注。'
  776. if (status === 'high' || status === '偏高') return '该指标偏高,建议咨询专业医生或营养师进行进一步评估。'
  777. if (status === 'low' || status === '偏低') return '该指标偏低,建议咨询专业医生或营养师进行进一步评估。'
  778. if (status === '缺乏' || status === '不足') return '该指标不足,建议通过饮食或补充剂适当补充。'
  779. if (status === 'abnormal') return '该指标异常,建议咨询专业医生进行详细检查。'
  780. if (status === '注意') return '该指标需要关注,建议定期监测并咨询专业人士。'
  781. return '请参考专业医生的建议进行解读。'
  782. },
  783. riskLevelText(level) {
  784. if (level === 'high') return '高风险'
  785. if (level === 'medium' || level === 'moderate') return '中风险'
  786. return '低风险'
  787. },
  788. doConfirm() {
  789. if (!this.canConfirm) return
  790. var self = this
  791. self.confirming = true
  792. uni.showLoading({ title: '确认入库并生成方案...' })
  793. var postData = {}
  794. if (self.payload.type === 'tongue') {
  795. // 舌诊:后端 /confirm?type=tongue 期望顶层 recordId + indicators,非 draftId
  796. postData.recordId = self.payload.recordId
  797. postData.indicators = self.payload.indicators
  798. } else {
  799. postData.draftId = self.draftId
  800. postData.payload = self.payload
  801. }
  802. // 优先级: 已选成员 > 新建成员 > 自动匹配(高置信度) > 子ID
  803. if (self.selectedMemberId) {
  804. postData.subjectId = self.selectedMemberId
  805. } else if (self.createdMemberId) {
  806. postData.subjectId = self.createdMemberId
  807. } else if (self.matchedMemberId) {
  808. postData.subjectId = self.matchedMemberId
  809. } else if (self.memberId) {
  810. postData.subjectId = self.memberId
  811. }
  812. var confirmFn = self.payload.type === 'tongue' ? confirmTongue : confirmReportPreview
  813. confirmFn(postData).then(function(res) {
  814. uni.hideLoading()
  815. if (res.code === 200) {
  816. var data = res.data || {}
  817. var reportId = data.reportId || (data.report && data.report.id)
  818. var planId = data.planId
  819. var goReport = function() {
  820. if (!reportId) {
  821. uni.navigateBack()
  822. return
  823. }
  824. var reportType = data.reportType || ''
  825. var gotoUrl
  826. if (reportType === 'gut_flora') {
  827. gotoUrl = '/pages/health/gut-flora-detail?reportId=' + reportId
  828. } else if (reportType === 'dan') {
  829. gotoUrl = '/pages/dan-assessment/report-result?reportId=' + reportId
  830. } else {
  831. gotoUrl = '/pages/health/report-detail?reportType=' + reportType + '&reportId=' + reportId
  832. }
  833. uni.redirectTo({ url: gotoUrl })
  834. }
  835. if (reportId && planId) {
  836. uni.showModal({
  837. title: '报告已入库',
  838. content: '方案已自动生成,是否立即查看并确认任务?',
  839. confirmText: '查看方案',
  840. cancelText: '看报告',
  841. success: function(modalRes) {
  842. if (modalRes.confirm) {
  843. uni.redirectTo({ url: '/pages/health/plan-list' })
  844. } else {
  845. goReport()
  846. }
  847. }
  848. })
  849. } else if (reportId) {
  850. uni.showToast({ title: '入库成功', icon: 'success' })
  851. setTimeout(goReport, 500)
  852. } else {
  853. uni.navigateBack()
  854. }
  855. } else {
  856. uni.showToast({ title: res.message || '确认失败', icon: 'none' })
  857. }
  858. }).catch(function(e) {
  859. uni.hideLoading()
  860. uni.showToast({ title: '确认失败,请重试', icon: 'none' })
  861. console.log('确认入库失败', e)
  862. }).finally(function() {
  863. self.confirming = false
  864. })
  865. },
  866. doDiscard() {
  867. var self = this
  868. uni.showModal({
  869. title: '确认放弃',
  870. content: '放弃后解析数据将被删除,需要重新上传',
  871. success: function(res) {
  872. if (res.confirm) {
  873. var discardFn = self.payload.type === 'tongue' ? discardTongue : discardReportDraft
  874. var discardArg = self.payload.type === 'tongue' ? {recordId: self.payload.recordId} : self.draftId
  875. discardFn(discardArg).then(function() {
  876. uni.showToast({ title: '已放弃', icon: 'none' })
  877. setTimeout(function() { uni.navigateBack() }, 500)
  878. }).catch(function() {
  879. uni.showToast({ title: '操作失败', icon: 'none' })
  880. })
  881. }
  882. }
  883. })
  884. },
  885. goBack() {
  886. uni.navigateBack()
  887. },
  888. hasTaxonomyData() {
  889. return this.taxonomyTotalCount > 0
  890. },
  891. hasPathogenData() {
  892. return this.pathogenTotalCount > 0
  893. },
  894. // taxonomy/病原菌列表 key(微信小程序禁止 :key 表达式)
  895. getTaxonomyKey(prefix, idx) {
  896. return prefix + idx
  897. },
  898. // 舌诊指标标签映射
  899. indicatorLabel(code) {
  900. var map = {
  901. 'tongue_color': '舌色',
  902. 'coating_color': '苔色',
  903. 'coating_texture': '苔质',
  904. 'fissure': '裂纹',
  905. 'teeth_mark': '齿痕',
  906. 'sublingual_vein': '舌下静脉',
  907. 'constitution': '体质'
  908. }
  909. return map[code] || code
  910. }
  911. }
  912. }
  913. </script>
  914. <style scoped lang="scss">
  915. .confirm-container {
  916. min-height: 100vh;
  917. background: #F5F7FA;
  918. }
  919. .nav-placeholder { width: 80rpx; }
  920. .tip-banner {
  921. display: flex;
  922. flex-direction: row;
  923. align-items: center;
  924. background: #E8F5E9;
  925. margin: 20rpx 30rpx;
  926. padding: 20rpx 24rpx;
  927. border-radius: 16rpx;
  928. }
  929. .tip-icon { font-size: 32rpx; margin-right: 16rpx; flex-shrink: 0; }
  930. .tip-text { font-size: calc(24rpx * var(--font-scale, 1)); color: #2E7D32; line-height: 1.5; }
  931. .parsing-banner {
  932. display: flex;
  933. flex-direction: row;
  934. align-items: center;
  935. background: #FFF8E1;
  936. margin: 0 30rpx 20rpx;
  937. padding: 20rpx 24rpx;
  938. border-radius: 16rpx;
  939. }
  940. .parsing-icon { font-size: 32rpx; margin-right: 16rpx; flex-shrink: 0; }
  941. .parsing-text { font-size: calc(24rpx * var(--font-scale, 1)); color: #F57F17; line-height: 1.5; }
  942. .form-section { margin: 0 30rpx; }
  943. .card {
  944. background: #fff;
  945. border-radius: 16rpx;
  946. padding: 24rpx;
  947. margin-bottom: 20rpx;
  948. box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.04);
  949. }
  950. .card-title {
  951. font-size: calc(28rpx * var(--font-scale, 1));
  952. font-weight: bold;
  953. color: #333;
  954. margin-bottom: 20rpx;
  955. padding-bottom: 12rpx;
  956. border-bottom: 2rpx solid #F0F0F0;
  957. display: flex;
  958. flex-direction: row;
  959. align-items: center;
  960. flex-wrap: wrap;
  961. gap: 4rpx;
  962. }
  963. .title-count {
  964. font-size: 24rpx;
  965. font-weight: normal;
  966. color: #94A3B8;
  967. margin-left: 8rpx;
  968. }
  969. .title-abnormal {
  970. font-size: calc(24rpx * var(--font-scale, 1));
  971. font-weight: normal;
  972. color: #C62828;
  973. }
  974. .card-title-actions {
  975. margin-left: auto;
  976. display: flex;
  977. flex-direction: row;
  978. gap: 16rpx;
  979. }
  980. .expand-all-btn {
  981. font-size: 22rpx;
  982. color: $brand-primary;
  983. padding: 4rpx 12rpx;
  984. border: 2rpx solid $brand-primary;
  985. border-radius: 20rpx;
  986. }
  987. .expand-all-btn:active { opacity: 0.7; }
  988. /* ===== 指标分组折叠 ===== */
  989. .indicator-group {
  990. border: 2rpx solid #E8EDF2;
  991. border-radius: 12rpx;
  992. margin-bottom: 12rpx;
  993. overflow: hidden;
  994. }
  995. .indicator-group-header {
  996. display: flex;
  997. flex-direction: row;
  998. align-items: center;
  999. justify-content: space-between;
  1000. padding: 20rpx 16rpx;
  1001. background: #F1F5F9;
  1002. }
  1003. .indicator-group-header:active { opacity: 0.8; }
  1004. .indicator-group-header-left {
  1005. display: flex;
  1006. flex-direction: row;
  1007. align-items: center;
  1008. flex: 1;
  1009. min-width: 0;
  1010. }
  1011. .indicator-group-arrow {
  1012. font-size: 30rpx;
  1013. color: #94A3B8;
  1014. margin-right: 12rpx;
  1015. transform: rotate(0deg);
  1016. transition: transform 0.2s ease;
  1017. flex-shrink: 0;
  1018. }
  1019. .indicator-group-arrow.arrow-open {
  1020. transform: rotate(90deg);
  1021. }
  1022. .indicator-group-name {
  1023. font-size: calc(26rpx * var(--font-scale, 1));
  1024. color: #333;
  1025. font-weight: 600;
  1026. flex: 1;
  1027. min-width: 0;
  1028. white-space: nowrap;
  1029. overflow: hidden;
  1030. text-overflow: ellipsis;
  1031. }
  1032. .indicator-group-header-right {
  1033. display: flex;
  1034. flex-direction: row;
  1035. align-items: center;
  1036. gap: 12rpx;
  1037. flex-shrink: 0;
  1038. margin-left: 12rpx;
  1039. }
  1040. .indicator-group-need {
  1041. font-size: calc(20rpx * var(--font-scale, 1));
  1042. color: #E65100;
  1043. background: #FFF3E0;
  1044. padding: 4rpx 12rpx;
  1045. border-radius: 20rpx;
  1046. font-weight: 500;
  1047. }
  1048. .indicator-group-count {
  1049. font-size: 22rpx;
  1050. color: #94A3B8;
  1051. }
  1052. .indicator-group-abnormal {
  1053. font-size: calc(22rpx * var(--font-scale, 1));
  1054. color: #C62828;
  1055. font-weight: 500;
  1056. margin-right: 8rpx;
  1057. }
  1058. .indicator-group-body {
  1059. padding: 16rpx;
  1060. }
  1061. .field-row {
  1062. display: flex;
  1063. flex-direction: row;
  1064. align-items: center;
  1065. margin-bottom: 16rpx;
  1066. }
  1067. .field-label {
  1068. font-size: calc(26rpx * var(--font-scale, 1));
  1069. color: #666;
  1070. width: 160rpx;
  1071. flex-shrink: 0;
  1072. }
  1073. .field-value {
  1074. font-size: calc(26rpx * var(--font-scale, 1));
  1075. color: #333;
  1076. font-weight: 500;
  1077. flex: 1;
  1078. }
  1079. .field-value.binding-required {
  1080. color: #E65100;
  1081. }
  1082. .member-select-row {
  1083. cursor: pointer;
  1084. }
  1085. .member-select-row:active {
  1086. opacity: 0.7;
  1087. }
  1088. .member-select-arrow {
  1089. font-size: 24rpx;
  1090. color: $brand-primary;
  1091. margin-left: 12rpx;
  1092. flex-shrink: 0;
  1093. }
  1094. .member-panel {
  1095. background: #F8FAFB;
  1096. border-radius: 12rpx;
  1097. padding: 16rpx;
  1098. margin: 8rpx 0 16rpx;
  1099. }
  1100. .auto-create-inline {
  1101. margin-top: 16rpx;
  1102. padding-top: 16rpx;
  1103. border-top: 2rpx solid #E3F2FD;
  1104. }
  1105. .field-input {
  1106. flex: 1;
  1107. font-size: calc(26rpx * var(--font-scale, 1));
  1108. color: #333;
  1109. padding: 8rpx 16rpx;
  1110. background: #F5F7FA;
  1111. border-radius: 8rpx;
  1112. }
  1113. .field-textarea {
  1114. width: 100%;
  1115. min-height: 120rpx;
  1116. font-size: calc(26rpx * var(--font-scale, 1));
  1117. color: #333;
  1118. padding: 16rpx;
  1119. background: #F5F7FA;
  1120. border-radius: 12rpx;
  1121. box-sizing: border-box;
  1122. margin-top: 8rpx;
  1123. }
  1124. .member-list {
  1125. display: flex;
  1126. flex-direction: row;
  1127. flex-wrap: wrap;
  1128. gap: 16rpx;
  1129. }
  1130. .member-list-hint {
  1131. width: 100%;
  1132. font-size: calc(24rpx * var(--font-scale, 1));
  1133. color: #999;
  1134. margin-bottom: 8rpx;
  1135. }
  1136. .member-item {
  1137. display: flex;
  1138. flex-direction: column;
  1139. align-items: center;
  1140. padding: 20rpx 24rpx;
  1141. background: #F5F7FA;
  1142. border-radius: 16rpx;
  1143. border: 2rpx solid transparent;
  1144. min-width: 120rpx;
  1145. }
  1146. .member-selected {
  1147. background: #E3F2FD;
  1148. border-color: $brand-primary;
  1149. }
  1150. .member-avatar {
  1151. width: 64rpx;
  1152. height: 64rpx;
  1153. border-radius: 50%;
  1154. background: $brand-primary;
  1155. color: #fff;
  1156. font-size: calc(28rpx * var(--font-scale, 1));
  1157. font-weight: bold;
  1158. text-align: center;
  1159. line-height: 64rpx;
  1160. margin-bottom: 8rpx;
  1161. }
  1162. .member-name { font-size: calc(24rpx * var(--font-scale, 1)); color: #333; }
  1163. .indicator-item {
  1164. background: #F8FAFB;
  1165. border-radius: 12rpx;
  1166. padding: 16rpx;
  1167. margin-bottom: 12rpx;
  1168. }
  1169. .ind-header {
  1170. display: flex;
  1171. flex-direction: row;
  1172. align-items: center;
  1173. gap: 12rpx;
  1174. }
  1175. .ind-name {
  1176. flex: 1;
  1177. font-size: calc(26rpx * var(--font-scale, 1));
  1178. color: #333;
  1179. font-weight: 500;
  1180. padding: 4rpx 8rpx;
  1181. background: #fff;
  1182. border-radius: 6rpx;
  1183. }
  1184. .ind-value {
  1185. width: 160rpx;
  1186. font-size: 26rpx;
  1187. color: #1565C0;
  1188. font-weight: bold;
  1189. text-align: right;
  1190. padding: 4rpx 8rpx;
  1191. background: #fff;
  1192. border-radius: 6rpx;
  1193. }
  1194. .ind-detail {
  1195. display: flex;
  1196. flex-direction: row;
  1197. margin-top: 8rpx;
  1198. gap: 16rpx;
  1199. }
  1200. .ind-ref { font-size: calc(22rpx * var(--font-scale, 1)); color: #94A3B8; }
  1201. .ind-unit { font-size: calc(22rpx * var(--font-scale, 1)); color: #64748B; }
  1202. .ind-symptoms {
  1203. display: flex;
  1204. flex-direction: row;
  1205. align-items: center;
  1206. margin-top: 8rpx;
  1207. gap: 8rpx;
  1208. }
  1209. .ind-symptom-label { font-size: calc(22rpx * var(--font-scale, 1)); color: #94A3B8; flex-shrink: 0; }
  1210. .ind-symptom-input {
  1211. flex: 1;
  1212. font-size: calc(22rpx * var(--font-scale, 1));
  1213. color: #333;
  1214. padding: 4rpx 8rpx;
  1215. background: #fff;
  1216. border-radius: 6rpx;
  1217. }
  1218. .status-tag {
  1219. display: inline-block;
  1220. font-size: 20rpx;
  1221. padding: 4rpx 12rpx;
  1222. border-radius: 8rpx;
  1223. font-weight: 500;
  1224. }
  1225. .status-normal { background: #E8F5E9; color: #2E7D32; }
  1226. .status-high { background: #FFF3E0; color: #E65100; }
  1227. .status-low { background: #E3F2FD; color: #1565C0; }
  1228. .status-abnormal { background: #FFEBEE; color: #C62828; }
  1229. /* ===== 指标帮助按钮 ===== */
  1230. .ind-help-btn {
  1231. width: 36rpx;
  1232. height: 36rpx;
  1233. border-radius: 50%;
  1234. background: #E2E8F0;
  1235. color: #64748B;
  1236. font-size: 24rpx;
  1237. font-weight: bold;
  1238. text-align: center;
  1239. line-height: 36rpx;
  1240. flex-shrink: 0;
  1241. }
  1242. .ind-help-btn:active {
  1243. background: #CBD5E1;
  1244. }
  1245. /* ===== 指标输入框颜色状态 ===== */
  1246. .ind-name-normal, .ind-value-normal {
  1247. background: #fff;
  1248. }
  1249. .ind-name-high, .ind-value-high,
  1250. .ind-name-low, .ind-value-low,
  1251. .ind-name-abnormal, .ind-value-abnormal,
  1252. .ind-name-deficient, .ind-value-deficient,
  1253. .ind-name-insufficient, .ind-value-insufficient,
  1254. .ind-name-warning, .ind-value-warning,
  1255. .ind-name-high-risk, .ind-value-high-risk,
  1256. .ind-name-low-risk, .ind-value-low-risk,
  1257. .ind-name-not-detected, .ind-value-not-detected {
  1258. background: #FFF0F0;
  1259. color: #C62828;
  1260. }
  1261. /* ===== 指标帮助弹窗 ===== */
  1262. .help-overlay {
  1263. position: fixed;
  1264. top: 0;
  1265. left: 0;
  1266. right: 0;
  1267. bottom: 0;
  1268. background: rgba(0,0,0,0.4);
  1269. z-index: 999;
  1270. display: flex;
  1271. align-items: center;
  1272. justify-content: center;
  1273. }
  1274. .help-popup {
  1275. width: 580rpx;
  1276. max-height: 70vh;
  1277. background: #fff;
  1278. border-radius: 24rpx;
  1279. overflow: hidden;
  1280. }
  1281. .help-popup-header {
  1282. display: flex;
  1283. flex-direction: row;
  1284. align-items: center;
  1285. justify-content: space-between;
  1286. padding: 28rpx 30rpx;
  1287. border-bottom: 2rpx solid #F0F0F0;
  1288. }
  1289. .help-popup-title {
  1290. font-size: calc(30rpx * var(--font-scale, 1));
  1291. font-weight: bold;
  1292. color: #333;
  1293. flex: 1;
  1294. }
  1295. .help-popup-close {
  1296. font-size: 32rpx;
  1297. color: #999;
  1298. padding: 8rpx;
  1299. }
  1300. .help-popup-body {
  1301. padding: 24rpx 30rpx;
  1302. }
  1303. .help-row {
  1304. display: flex;
  1305. flex-direction: row;
  1306. align-items: center;
  1307. margin-bottom: 16rpx;
  1308. }
  1309. .help-label {
  1310. font-size: calc(26rpx * var(--font-scale, 1));
  1311. color: #94A3B8;
  1312. width: 140rpx;
  1313. flex-shrink: 0;
  1314. }
  1315. .help-value {
  1316. font-size: calc(28rpx * var(--font-scale, 1));
  1317. color: #333;
  1318. font-weight: 500;
  1319. flex: 1;
  1320. }
  1321. .help-status-normal { color: #2E7D32; }
  1322. .help-status-high { color: #C62828; }
  1323. .help-status-low { color: #1565C0; }
  1324. .help-status-abnormal { color: #C62828; }
  1325. .help-desc {
  1326. margin-top: 8rpx;
  1327. padding: 20rpx;
  1328. background: #F8FAFB;
  1329. border-radius: 12rpx;
  1330. }
  1331. .help-desc-label {
  1332. display: block;
  1333. font-size: calc(24rpx * var(--font-scale, 1));
  1334. color: #94A3B8;
  1335. margin-bottom: 8rpx;
  1336. }
  1337. .help-desc-text {
  1338. display: block;
  1339. font-size: calc(26rpx * var(--font-scale, 1));
  1340. color: #333;
  1341. line-height: 1.6;
  1342. }
  1343. .flora-item {
  1344. display: flex;
  1345. flex-direction: row;
  1346. align-items: center;
  1347. gap: 12rpx;
  1348. padding: 12rpx 16rpx;
  1349. background: #F8FAFB;
  1350. border-radius: 10rpx;
  1351. margin-bottom: 8rpx;
  1352. }
  1353. .flora-name {
  1354. flex: 1;
  1355. font-size: calc(26rpx * var(--font-scale, 1));
  1356. color: #333;
  1357. padding: 4rpx 8rpx;
  1358. background: #fff;
  1359. border-radius: 6rpx;
  1360. }
  1361. .flora-abundance {
  1362. width: 140rpx;
  1363. font-size: calc(26rpx * var(--font-scale, 1));
  1364. color: #1565C0;
  1365. text-align: right;
  1366. padding: 4rpx 8rpx;
  1367. background: #fff;
  1368. border-radius: 6rpx;
  1369. }
  1370. .taxonomy-group {
  1371. margin-bottom: 16rpx;
  1372. }
  1373. .taxonomy-group:last-child {
  1374. margin-bottom: 0;
  1375. }
  1376. .taxonomy-group-label {
  1377. display: block;
  1378. font-size: calc(24rpx * var(--font-scale, 1));
  1379. font-weight: 500;
  1380. color: #666;
  1381. padding: 8rpx 4rpx 6rpx;
  1382. }
  1383. .pathogen-group {
  1384. margin-bottom: 16rpx;
  1385. }
  1386. .pathogen-group:last-child {
  1387. margin-bottom: 0;
  1388. }
  1389. .pathogen-group-label {
  1390. display: block;
  1391. font-size: calc(24rpx * var(--font-scale, 1));
  1392. font-weight: 500;
  1393. color: #D32F2F;
  1394. padding: 8rpx 4rpx 6rpx;
  1395. }
  1396. .risk-item {
  1397. display: flex;
  1398. flex-direction: row;
  1399. align-items: center;
  1400. gap: 12rpx;
  1401. padding: 12rpx 16rpx;
  1402. background: #F8FAFB;
  1403. border-radius: 10rpx;
  1404. margin-bottom: 8rpx;
  1405. }
  1406. .risk-name {
  1407. flex: 1;
  1408. font-size: calc(26rpx * var(--font-scale, 1));
  1409. color: #333;
  1410. padding: 4rpx 8rpx;
  1411. background: #fff;
  1412. border-radius: 6rpx;
  1413. }
  1414. .risk-level {
  1415. font-size: calc(22rpx * var(--font-scale, 1));
  1416. font-weight: 500;
  1417. padding: 4rpx 12rpx;
  1418. border-radius: 8rpx;
  1419. }
  1420. .risk-low { background: #E8F5E9; color: #2E7D32; }
  1421. .risk-medium, .risk-moderate { background: #FFF3E0; color: #E65100; }
  1422. .risk-high { background: #FFEBEE; color: #C62828; }
  1423. .food-item {
  1424. display: flex;
  1425. flex-direction: row;
  1426. align-items: center;
  1427. gap: 12rpx;
  1428. padding: 12rpx 16rpx;
  1429. background: #F8FAFB;
  1430. border-radius: 10rpx;
  1431. margin-bottom: 8rpx;
  1432. }
  1433. .food-name {
  1434. flex: 1;
  1435. font-size: calc(26rpx * var(--font-scale, 1));
  1436. color: #333;
  1437. padding: 4rpx 8rpx;
  1438. background: #fff;
  1439. border-radius: 6rpx;
  1440. }
  1441. .food-category {
  1442. width: 160rpx;
  1443. font-size: calc(24rpx * var(--font-scale, 1));
  1444. color: #64748B;
  1445. text-align: right;
  1446. padding: 4rpx 8rpx;
  1447. background: #fff;
  1448. border-radius: 6rpx;
  1449. }
  1450. .action-row {
  1451. display: flex;
  1452. flex-direction: row;
  1453. gap: 20rpx;
  1454. margin-top: 10rpx;
  1455. margin-bottom: 20rpx;
  1456. }
  1457. .btn-discard {
  1458. flex: 1;
  1459. background: #fff;
  1460. border: 2rpx solid #EF4444;
  1461. color: #EF4444;
  1462. font-size: 28rpx;
  1463. font-weight: bold;
  1464. text-align: center;
  1465. padding: 24rpx;
  1466. border-radius: 16rpx;
  1467. }
  1468. .btn-discard:active { opacity: 0.7; }
  1469. .btn-confirm {
  1470. flex: 2;
  1471. background: linear-gradient(135deg, $brand-primary, $brand-primary-dark);
  1472. color: #fff;
  1473. font-size: 28rpx;
  1474. font-weight: bold;
  1475. text-align: center;
  1476. padding: 24rpx;
  1477. border-radius: 16rpx;
  1478. }
  1479. .btn-confirm:active { opacity: 0.85; }
  1480. .btn-disabled { opacity: 0.5; }
  1481. .bottom-spacer { height: 80rpx; }
  1482. /* ===== Phase 2: 匹配可信度 ===== */
  1483. .confidence-card {
  1484. margin: 0 30rpx 20rpx;
  1485. background: #fff;
  1486. border-radius: 16rpx;
  1487. padding: 24rpx;
  1488. box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.04);
  1489. }
  1490. .confidence-header {
  1491. display: flex;
  1492. flex-direction: row;
  1493. align-items: center;
  1494. justify-content: space-between;
  1495. margin-bottom: 16rpx;
  1496. }
  1497. .confidence-label {
  1498. font-size: calc(26rpx * var(--font-scale, 1));
  1499. color: #333;
  1500. font-weight: 500;
  1501. }
  1502. .confidence-badge {
  1503. font-size: 22rpx;
  1504. font-weight: bold;
  1505. padding: 4rpx 16rpx;
  1506. border-radius: 20rpx;
  1507. }
  1508. .badge-high { background: #E8F5E9; color: #2E7D32; }
  1509. .badge-medium { background: #FFF8E1; color: #F57F17; }
  1510. .badge-low { background: #FFF3E0; color: #E65100; }
  1511. .badge-none { background: #FFEBEE; color: #C62828; }
  1512. .confidence-bar-track {
  1513. height: 12rpx;
  1514. background: #E2E8F0;
  1515. border-radius: 6rpx;
  1516. overflow: hidden;
  1517. margin-bottom: 12rpx;
  1518. }
  1519. .confidence-bar-fill {
  1520. height: 100%;
  1521. border-radius: 6rpx;
  1522. transition: width 0.3s ease;
  1523. }
  1524. .fill-high { background: linear-gradient(90deg, #66BB6A, #43A047); }
  1525. .fill-medium { background: linear-gradient(90deg, #FFD54F, #FFC107); }
  1526. .fill-low { background: linear-gradient(90deg, #FF8A65, #FF5722); }
  1527. .fill-none { background: linear-gradient(90deg, #EF9A9A, #E53935); }
  1528. .confidence-desc {
  1529. font-size: calc(22rpx * var(--font-scale, 1));
  1530. color: #94A3B8;
  1531. line-height: 1.4;
  1532. }
  1533. /* ===== Phase 2: 候选成员列表 ===== */
  1534. .candidate-section {
  1535. margin-bottom: 16rpx;
  1536. }
  1537. .candidate-section-hint {
  1538. display: block;
  1539. font-size: calc(24rpx * var(--font-scale, 1));
  1540. color: #64748B;
  1541. margin-bottom: 12rpx;
  1542. }
  1543. .candidate-item {
  1544. display: flex;
  1545. flex-direction: row;
  1546. align-items: center;
  1547. justify-content: space-between;
  1548. padding: 16rpx;
  1549. background: #F8FAFB;
  1550. border-radius: 12rpx;
  1551. margin-bottom: 10rpx;
  1552. border: 2rpx solid transparent;
  1553. }
  1554. .candidate-item:active { opacity: 0.8; }
  1555. .candidate-selected {
  1556. background: #E3F2FD;
  1557. border-color: $brand-primary;
  1558. }
  1559. .candidate-left {
  1560. display: flex;
  1561. flex-direction: row;
  1562. align-items: center;
  1563. flex: 1;
  1564. min-width: 0;
  1565. }
  1566. .candidate-avatar {
  1567. width: 56rpx;
  1568. height: 56rpx;
  1569. border-radius: 50%;
  1570. background: $brand-primary;
  1571. color: #fff;
  1572. font-size: calc(24rpx * var(--font-scale, 1));
  1573. font-weight: bold;
  1574. text-align: center;
  1575. line-height: 56rpx;
  1576. margin-right: 16rpx;
  1577. flex-shrink: 0;
  1578. }
  1579. .candidate-info {
  1580. flex: 1;
  1581. min-width: 0;
  1582. }
  1583. .candidate-name {
  1584. font-size: calc(26rpx * var(--font-scale, 1));
  1585. color: #333;
  1586. font-weight: 500;
  1587. display: block;
  1588. }
  1589. .candidate-detail {
  1590. font-size: calc(22rpx * var(--font-scale, 1));
  1591. color: #94A3B8;
  1592. display: block;
  1593. }
  1594. .candidate-fields {
  1595. font-size: calc(20rpx * var(--font-scale, 1));
  1596. color: $brand-primary;
  1597. display: block;
  1598. margin-top: 2rpx;
  1599. }
  1600. .candidate-right {
  1601. display: flex;
  1602. flex-direction: row;
  1603. align-items: center;
  1604. gap: 12rpx;
  1605. flex-shrink: 0;
  1606. margin-left: 16rpx;
  1607. }
  1608. .score-bar-track {
  1609. width: 80rpx;
  1610. height: 10rpx;
  1611. background: #E2E8F0;
  1612. border-radius: 5rpx;
  1613. overflow: hidden;
  1614. }
  1615. .score-bar-fill {
  1616. height: 100%;
  1617. background: linear-gradient(90deg, $brand-primary, $brand-primary-dark);
  1618. border-radius: 5rpx;
  1619. }
  1620. .score-text {
  1621. font-size: calc(24rpx * var(--font-scale, 1));
  1622. color: #1565C0;
  1623. font-weight: bold;
  1624. min-width: 48rpx;
  1625. text-align: right;
  1626. }
  1627. /* ===== Phase 2: 新建成员入口 ===== */
  1628. .create-member-entry {
  1629. display: flex;
  1630. flex-direction: row;
  1631. align-items: center;
  1632. justify-content: center;
  1633. padding: 20rpx;
  1634. border: 2rpx dashed #CBD5E1;
  1635. border-radius: 12rpx;
  1636. margin-top: 8rpx;
  1637. }
  1638. .create-member-entry:active { background: #F1F5F9; }
  1639. .create-member-icon {
  1640. font-size: 28rpx;
  1641. color: $brand-primary;
  1642. font-weight: bold;
  1643. margin-right: 8rpx;
  1644. }
  1645. .create-member-text {
  1646. font-size: calc(24rpx * var(--font-scale, 1));
  1647. color: $brand-primary;
  1648. }
  1649. /* ===== Phase 2: 新建成员表单(面板内嵌) ===== */
  1650. .form-item {
  1651. display: flex;
  1652. flex-direction: row;
  1653. align-items: center;
  1654. margin-bottom: 20rpx;
  1655. }
  1656. .form-item-label {
  1657. font-size: calc(26rpx * var(--font-scale, 1));
  1658. color: #333;
  1659. width: 100rpx;
  1660. flex-shrink: 0;
  1661. }
  1662. .form-item-input {
  1663. flex: 1;
  1664. font-size: calc(26rpx * var(--font-scale, 1));
  1665. color: #333;
  1666. padding: 12rpx 16rpx;
  1667. background: #F5F7FA;
  1668. border-radius: 10rpx;
  1669. }
  1670. .gender-selector {
  1671. display: flex;
  1672. flex-direction: row;
  1673. gap: 16rpx;
  1674. }
  1675. .gender-option {
  1676. font-size: calc(26rpx * var(--font-scale, 1));
  1677. color: #333;
  1678. padding: 12rpx 32rpx;
  1679. background: #F5F7FA;
  1680. border-radius: 10rpx;
  1681. border: 2rpx solid transparent;
  1682. }
  1683. .gender-option:active { opacity: 0.7; }
  1684. .gender-selected {
  1685. background: #E3F2FD;
  1686. border-color: $brand-primary;
  1687. color: #1565C0;
  1688. }
  1689. .btn-create-submit {
  1690. background: linear-gradient(135deg, $brand-primary, $brand-primary-dark);
  1691. color: #fff;
  1692. font-size: 28rpx;
  1693. font-weight: bold;
  1694. text-align: center;
  1695. padding: 22rpx;
  1696. border-radius: 14rpx;
  1697. margin-top: 10rpx;
  1698. }
  1699. .btn-create-submit:active { opacity: 0.85; }
  1700. /* ===== 舌诊指标 ===== */
  1701. .tongue-indicator-grid {
  1702. display: flex;
  1703. flex-direction: row;
  1704. flex-wrap: wrap;
  1705. gap: 12rpx;
  1706. }
  1707. .tongue-ind-item {
  1708. width: calc(50% - 6rpx);
  1709. background: #F8FAFC;
  1710. border-radius: 16rpx;
  1711. padding: 20rpx 24rpx;
  1712. box-sizing: border-box;
  1713. }
  1714. .tongue-ind-label {
  1715. display: block;
  1716. font-size: calc(24rpx * var(--font-scale, 1));
  1717. color: #94A3B8;
  1718. margin-bottom: 8rpx;
  1719. }
  1720. .tongue-ind-value {
  1721. display: block;
  1722. font-size: 28rpx;
  1723. color: #1E293B;
  1724. font-weight: 500;
  1725. }
  1726. .tongue-summary {
  1727. margin-top: 20rpx;
  1728. padding: 20rpx 24rpx;
  1729. background: #FFF8E1;
  1730. border-radius: 16rpx;
  1731. }
  1732. .tongue-summary-title {
  1733. font-size: calc(24rpx * var(--font-scale, 1));
  1734. color: #F59E0B;
  1735. font-weight: 600;
  1736. display: block;
  1737. margin-bottom: 8rpx;
  1738. }
  1739. .tongue-summary-text {
  1740. font-size: calc(26rpx * var(--font-scale, 1));
  1741. color: #333;
  1742. line-height: 1.6;
  1743. }
  1744. /* ===== 报告反馈入口 ===== */
  1745. .feedback-entry {
  1746. position: fixed;
  1747. right: 30rpx;
  1748. bottom: 60rpx;
  1749. display: flex;
  1750. flex-direction: row;
  1751. align-items: center;
  1752. background: linear-gradient(135deg, $brand-primary, $brand-primary-dark);
  1753. border-radius: 44rpx;
  1754. padding: 18rpx 30rpx;
  1755. box-shadow: 0 4rpx 16rpx rgba(59, 130, 246, 0.4);
  1756. z-index: 90;
  1757. }
  1758. .feedback-entry-icon { font-size: 32rpx; margin-right: 10rpx; }
  1759. .feedback-entry-text { font-size: calc(28rpx * var(--font-scale, 1)); color: #fff; font-weight: 500; }
  1760. .feedback-entry-arrow { font-size: 30rpx; color: #fff; margin-left: 6rpx; }
  1761. .feedback-entry:active { opacity: 0.85; }
  1762. </style>