report-confirm.vue 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096
  1. <template>
  2. <view class="confirm-container">
  3. <view class="nav-bar">
  4. <text class="nav-back" @click="goBack">‹ 返回</text>
  5. <text class="nav-title">确认健康报告</text>
  6. <view class="nav-placeholder"></view>
  7. </view>
  8. <view class="tip-banner">
  9. <text class="tip-icon">✅</text>
  10. <text class="tip-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">
  28. <text class="field-label">被检测人</text>
  29. <text class="field-value" v-if="!needBind">{{ extractedName || '未识别' }}</text>
  30. <text class="field-value binding-required" v-else>需手动绑定</text>
  31. </view>
  32. <view class="field-row" v-if="extractedGender">
  33. <text class="field-label">性别</text>
  34. <text class="field-value">{{ extractedGender === 'male' ? '男' : extractedGender === 'female' ? '女' : extractedGender }}</text>
  35. </view>
  36. <view class="field-row" v-if="extractedAge">
  37. <text class="field-label">年龄</text>
  38. <text class="field-value">{{ extractedAge }}</text>
  39. </view>
  40. <view class="field-row" v-if="payload.summary && payload.type !== 'tongue'">
  41. <text class="field-label">综合评分</text>
  42. <input class="field-input" type="number" v-model="payload.summary.overallScore" />
  43. </view>
  44. <view class="field-row" v-if="payload.summary && payload.type !== 'tongue'">
  45. <text class="field-label">报告日期</text>
  46. <input class="field-input" type="text" v-model="payload.summary.reportDate" placeholder="如: 2025-06-15" />
  47. </view>
  48. <view class="field-row" v-if="payload.summary && payload.type !== 'tongue'">
  49. <text class="field-label">报告摘要</text>
  50. <textarea class="field-textarea" v-model="payload.summary.summary" maxlength="500" />
  51. </view>
  52. </view>
  53. <!-- 家庭成员绑定(含候选推荐) -->
  54. <view class="card" v-if="needBind">
  55. <view class="card-title">选择家庭成员</view>
  56. <!-- 候选匹配列表(按置信度排序) -->
  57. <view class="candidate-section" v-if="candidates.length > 0">
  58. <text class="candidate-section-hint">以下成员与报告信息匹配,请选择:</text>
  59. <view
  60. class="candidate-item"
  61. v-for="(cand, idx) in candidates"
  62. :key="cand.id || idx"
  63. :class="{ 'candidate-selected': selectedMemberId === (cand.id || cand.userId) }"
  64. @click="selectCandidate(cand)"
  65. >
  66. <view class="candidate-left">
  67. <text class="candidate-avatar">{{ (cand.nickname || cand.name || '?').substring(0, 1) }}</text>
  68. <view class="candidate-info">
  69. <text class="candidate-name">{{ cand.nickname || cand.name || '未知' }}</text>
  70. <text class="candidate-detail" v-if="cand.gender || cand.age">{{ genderText(cand.gender) }}{{ cand.age ? ' · ' + cand.age + '岁' : '' }}</text>
  71. <text class="candidate-fields" v-if="cand.matchFields">匹配: {{ cand.matchFields }}</text>
  72. </view>
  73. </view>
  74. <view class="candidate-right">
  75. <view class="score-bar-track">
  76. <view class="score-bar-fill" :style="'width:' + (cand.similarityScore * 100) + '%'"></view>
  77. </view>
  78. <text class="score-text">{{ Math.round(cand.similarityScore * 100) }}%</text>
  79. </view>
  80. </view>
  81. </view>
  82. <!-- 全量家庭成员列表(后备) -->
  83. <view class="member-list" v-if="familyMembers.length > 0 && (candidates.length === 0 || !showCandidatesOnly)">
  84. <view
  85. class="member-item"
  86. v-for="member in familyMembers"
  87. :key="member.userId || member.id"
  88. :class="{ 'member-selected': selectedMemberId === (member.userId || member.id) }"
  89. @click="selectMember(member)"
  90. >
  91. <text class="member-avatar">{{ (member.nickname || '?').substring(0, 1) }}</text>
  92. <text class="member-name">{{ member.nickname || '未知' }}</text>
  93. </view>
  94. </view>
  95. <!-- 新建成员入口 -->
  96. <view class="create-member-entry" @click="toggleAutoCreate">
  97. <text class="create-member-icon">+</text>
  98. <text class="create-member-text">{{ showAutoCreateForm ? '收起新建表单' : '不在列表中?新建家庭成员' }}</text>
  99. </view>
  100. </view>
  101. <!-- 新建家庭成员表单 -->
  102. <view class="card auto-create-card" v-if="showAutoCreateForm">
  103. <view class="card-title">新建家庭成员</view>
  104. <view class="form-item">
  105. <text class="form-item-label">姓名</text>
  106. <input class="form-item-input" v-model="newMember.name" placeholder="请输入姓名" />
  107. </view>
  108. <view class="form-item">
  109. <text class="form-item-label">性别</text>
  110. <view class="gender-selector">
  111. <text class="gender-option" :class="{ 'gender-selected': newMember.gender === 'male' }" @click="newMember.gender = 'male'">男</text>
  112. <text class="gender-option" :class="{ 'gender-selected': newMember.gender === 'female' }" @click="newMember.gender = 'female'">女</text>
  113. </view>
  114. </view>
  115. <view class="form-item">
  116. <text class="form-item-label">年龄</text>
  117. <input class="form-item-input" type="number" v-model="newMember.age" placeholder="请输入年龄" />
  118. </view>
  119. <view class="btn-create-submit" :class="{ 'btn-disabled': creatingMember }" @click="doAutoCreate">
  120. <text v-if="creatingMember">创建中...</text>
  121. <text v-else>确认创建</text>
  122. </view>
  123. </view>
  124. <!-- 舌诊指标(只读展示) -->
  125. <view class="card" v-if="payload.type === 'tongue' && payload.indicators && payload.indicators.length">
  126. <view class="card-title">舌象指标</view>
  127. <view class="tongue-indicator-grid">
  128. <view class="tongue-ind-item" v-for="(ind, idx) in payload.indicators" :key="idx">
  129. <text class="tongue-ind-label">{{ indicatorLabel(ind.code) }}</text>
  130. <text class="tongue-ind-value">{{ ind.value }}</text>
  131. </view>
  132. </view>
  133. <view class="tongue-summary" v-if="payload.summary && payload.summary.overallAssessment">
  134. <text class="tongue-summary-title">综合评估</text>
  135. <text class="tongue-summary-text">{{ payload.summary.overallAssessment }}</text>
  136. </view>
  137. </view>
  138. <!-- 健康指标(舌诊时隐藏,使用上方舌象指标卡片) -->
  139. <view class="card" v-if="payload.type !== 'tongue' && payload.indicators && payload.indicators.length">
  140. <view class="card-title">健康指标 ({{ payload.indicators.length }})</view>
  141. <view class="indicator-item" v-for="(ind, idx) in payload.indicators" :key="idx">
  142. <view class="ind-header">
  143. <input class="ind-name" v-model="payload.indicators[idx].name" />
  144. <input class="ind-value" v-model="payload.indicators[idx].value" />
  145. </view>
  146. <view class="ind-detail" v-if="payload.indicators[idx].unit || payload.indicators[idx].referenceRange">
  147. <text class="ind-ref" v-if="payload.indicators[idx].referenceRange">参考: {{ payload.indicators[idx].referenceRange }}</text>
  148. <text class="ind-unit" v-if="payload.indicators[idx].unit">{{ payload.indicators[idx].unit }}</text>
  149. </view>
  150. <view class="ind-symptoms" v-if="payload.indicators[idx].symptoms">
  151. <text class="ind-symptom-label">症状:</text>
  152. <input class="ind-symptom-input" v-model="payload.indicators[idx].symptoms" />
  153. </view>
  154. <view class="ind-status-row">
  155. <view class="status-tag" :class="'status-' + (payload.indicators[idx].status || 'normal')">
  156. {{ statusText(payload.indicators[idx].status) }}
  157. </view>
  158. </view>
  159. </view>
  160. </view>
  161. <!-- 肠道菌群 -->
  162. <view class="card" v-if="payload.gutFlora && payload.gutFlora.length">
  163. <view class="card-title">肠道菌群 ({{ payload.gutFlora.length }})</view>
  164. <view class="flora-item" v-for="(flora, idx) in payload.gutFlora" :key="idx">
  165. <input class="flora-name" v-model="payload.gutFlora[idx].name" />
  166. <input class="flora-abundance" type="digit" v-model="payload.gutFlora[idx].abundance" />
  167. <view class="status-tag" :class="'status-' + (flora.status || 'normal')">
  168. {{ statusText(flora.status) }}
  169. </view>
  170. </view>
  171. </view>
  172. <!-- 疾病风险 -->
  173. <view class="card" v-if="payload.diseaseRisks && payload.diseaseRisks.length">
  174. <view class="card-title">疾病风险 ({{ payload.diseaseRisks.length }})</view>
  175. <view class="risk-item" v-for="(risk, idx) in payload.diseaseRisks" :key="idx">
  176. <input class="risk-name" v-model="payload.diseaseRisks[idx].diseaseName" />
  177. <view class="risk-level" :class="'risk-' + (risk.riskLevel || 'low')">
  178. {{ riskLevelText(risk.riskLevel) }}
  179. </view>
  180. </view>
  181. </view>
  182. <!-- 饮食推荐 -->
  183. <view class="card" v-if="payload.foods && payload.foods.length">
  184. <view class="card-title">饮食推荐 ({{ payload.foods.length }})</view>
  185. <view class="food-item" v-for="(food, idx) in payload.foods" :key="idx">
  186. <input class="food-name" v-model="payload.foods[idx].foodName" />
  187. <input class="food-category" v-model="payload.foods[idx].category" />
  188. </view>
  189. </view>
  190. <!-- 操作按钮 -->
  191. <view class="action-row">
  192. <view class="btn-discard" @click="doDiscard">
  193. <text>放弃</text>
  194. </view>
  195. <view class="btn-confirm" :class="{ 'btn-disabled': !canConfirm }" @click="doConfirm">
  196. <text v-if="confirming">确认中...</text>
  197. <text v-else>确认入库</text>
  198. </view>
  199. </view>
  200. </view>
  201. <view class="bottom-spacer"></view>
  202. </view>
  203. </template>
  204. <script>
  205. import { confirmReportPreview, discardReportDraft, confirmTongue, discardTongue } from '../../utils/api.js'
  206. import { addFamilyMember } from '../../utils/api.js'
  207. export default {
  208. data() {
  209. return {
  210. draftId: null,
  211. childId: null,
  212. familyId: null,
  213. payload: {
  214. summary: {},
  215. indicators: [],
  216. gutFlora: [],
  217. probioticSpecies: [],
  218. foods: [],
  219. diseaseRisks: []
  220. },
  221. extractedName: '',
  222. extractedGender: '',
  223. extractedAge: '',
  224. matchedMemberId: null,
  225. needBind: false,
  226. selectedMemberId: null,
  227. familyMembers: [],
  228. confirming: false,
  229. // Phase 2: 匹配可信度与候选
  230. matchConfidence: null,
  231. matchLabel: '',
  232. candidates: [],
  233. showCandidatesOnly: false,
  234. // 新建成员
  235. showAutoCreateForm: false,
  236. newMember: {
  237. name: '',
  238. gender: '',
  239. age: ''
  240. },
  241. creatingMember: false,
  242. createdMemberId: null
  243. }
  244. },
  245. computed: {
  246. canConfirm() {
  247. if (this.needBind && !this.selectedMemberId && !this.createdMemberId) return false
  248. return this.draftId && !this.confirming
  249. },
  250. confidenceLevelClass() {
  251. if (!this.matchConfidence && this.matchConfidence !== 0) return ''
  252. var val = parseFloat(this.matchConfidence)
  253. if (this.matchLabel === 'exact' || val >= 0.8) return 'high'
  254. if (this.matchLabel === 'fuzzy' || val >= 0.5) return 'medium'
  255. if (this.matchLabel === 'profile_match' || val >= 0.2) return 'low'
  256. return 'none'
  257. },
  258. confidenceLevelText() {
  259. var map = { high: '高', medium: '中', low: '低', none: '未匹配' }
  260. return map[this.confidenceLevelClass] || ''
  261. },
  262. confidencePercent() {
  263. if (!this.matchConfidence && this.matchConfidence !== 0) return 0
  264. return Math.round(parseFloat(this.matchConfidence) * 100)
  265. },
  266. confidenceDesc() {
  267. if (this.matchLabel === 'exact') return '根据姓名、性别、年龄自动精确匹配到家庭成员'
  268. if (this.matchLabel === 'fuzzy') return '部分字段匹配,请确认是否为以下家庭成员'
  269. if (this.matchLabel === 'profile_match') return '根据档案信息推测匹配,请手动确认'
  270. if (this.matchLabel === 'no_match') return '无法匹配到现有家庭成员,可新建成员'
  271. return ''
  272. }
  273. },
  274. onLoad: function(options) {
  275. this.draftId = options.draftId ? parseInt(options.draftId) : null
  276. this.childId = options.childId ? parseInt(options.childId) : null
  277. this.familyId = options.familyId ? parseInt(options.familyId) : null
  278. var payloadStr = decodeURIComponent(options.payload || '{}')
  279. try {
  280. var parsed = typeof payloadStr === 'string' ? JSON.parse(payloadStr) : payloadStr
  281. this.payload = Object.assign({
  282. summary: {},
  283. indicators: [],
  284. gutFlora: [],
  285. probioticSpecies: [],
  286. foods: [],
  287. diseaseRisks: []
  288. }, parsed)
  289. } catch (e) {
  290. console.log('解析payload失败', e)
  291. }
  292. this.extractedName = options.extractedName || ''
  293. this.extractedGender = options.extractedGender || ''
  294. this.extractedAge = options.extractedAge || ''
  295. this.matchedMemberId = options.matchedMemberId ? parseInt(options.matchedMemberId) : null
  296. // 从解析结果填充报告日期
  297. var extractedReportDate = options.extractedReportDate || ''
  298. if (extractedReportDate && this.payload.summary) {
  299. this.payload.summary.reportDate = extractedReportDate
  300. }
  301. this.needBind = options.needBind === 'true' || options.needBind === '1'
  302. this.matchConfidence = options.matchConfidence ? parseFloat(options.matchConfidence) : null
  303. this.matchLabel = decodeURIComponent(options.matchLabel || '')
  304. // 解析候选成员列表
  305. var candidatesStr = decodeURIComponent(options.candidates || '')
  306. if (candidatesStr) {
  307. try {
  308. this.candidates = JSON.parse(candidatesStr)
  309. } catch (e) {
  310. this.candidates = []
  311. }
  312. }
  313. // 有候选时优先展示候选列表
  314. if (this.candidates.length > 0) {
  315. this.showCandidatesOnly = true
  316. }
  317. if (this.needBind) {
  318. this.loadFamilyMembers()
  319. // 高置信度且已匹配 → 直接选中
  320. if (this.matchedMemberId && this.matchConfidence >= 0.8) {
  321. this.selectedMemberId = this.matchedMemberId
  322. }
  323. }
  324. },
  325. methods: {
  326. loadFamilyMembers() {
  327. var store = uni.getStorageSync('familyChildren')
  328. if (store) {
  329. try {
  330. this.familyMembers = typeof store === 'string' ? JSON.parse(store) : store
  331. } catch (e) {
  332. this.familyMembers = []
  333. }
  334. }
  335. },
  336. selectMember(member) {
  337. this.selectedMemberId = member.userId || member.id
  338. },
  339. // === Phase 2: 候选匹配与新建成员 ===
  340. genderText(gender) {
  341. if (gender === 'male') return '男'
  342. if (gender === 'female') return '女'
  343. return gender || ''
  344. },
  345. selectCandidate(candidate) {
  346. this.selectedMemberId = candidate.id || candidate.userId
  347. this.showAutoCreateForm = false
  348. },
  349. toggleAutoCreate() {
  350. this.showAutoCreateForm = !this.showAutoCreateForm
  351. if (this.showAutoCreateForm) {
  352. this.newMember.name = this.extractedName || ''
  353. this.newMember.gender = this.extractedGender || ''
  354. this.newMember.age = this.extractedAge || ''
  355. this.selectedMemberId = null
  356. }
  357. },
  358. doAutoCreate() {
  359. var self = this
  360. if (!this.newMember.name) {
  361. uni.showToast({ title: '请输入成员姓名', icon: 'none' })
  362. return
  363. }
  364. self.creatingMember = true
  365. uni.showLoading({ title: '创建成员...' })
  366. addFamilyMember({
  367. nickname: self.newMember.name,
  368. gender: self.newMember.gender || '',
  369. age: self.newMember.age ? parseInt(self.newMember.age) : null
  370. }).then(function(res) {
  371. uni.hideLoading()
  372. if (res.code === 200) {
  373. var memberId = res.data && (res.data.userId || res.data.id)
  374. if (memberId) {
  375. self.createdMemberId = memberId
  376. self.selectedMemberId = memberId
  377. self.showAutoCreateForm = false
  378. uni.showToast({ title: '创建成功', icon: 'success' })
  379. }
  380. } else {
  381. uni.showToast({ title: res.message || '创建失败', icon: 'none' })
  382. }
  383. }).catch(function(e) {
  384. uni.hideLoading()
  385. uni.showToast({ title: '创建失败', icon: 'none' })
  386. }).finally(function() {
  387. self.creatingMember = false
  388. })
  389. },
  390. statusText(status) {
  391. if (status === 'high') return '偏高'
  392. if (status === 'low') return '偏低'
  393. if (status === 'abnormal') return '异常'
  394. return '正常'
  395. },
  396. riskLevelText(level) {
  397. if (level === 'high') return '高风险'
  398. if (level === 'medium' || level === 'moderate') return '中风险'
  399. return '低风险'
  400. },
  401. doConfirm() {
  402. if (!this.canConfirm) return
  403. var self = this
  404. self.confirming = true
  405. uni.showLoading({ title: '确认入库...' })
  406. var postData = {
  407. draftId: self.draftId,
  408. payload: self.payload
  409. }
  410. // 优先级: 已选成员 > 新建成员 > 自动匹配(高置信度) > 子ID
  411. if (self.selectedMemberId) {
  412. postData.subjectId = self.selectedMemberId
  413. } else if (self.createdMemberId) {
  414. postData.subjectId = self.createdMemberId
  415. } else if (self.matchedMemberId) {
  416. postData.subjectId = self.matchedMemberId
  417. } else if (self.childId) {
  418. postData.subjectId = self.childId
  419. }
  420. var confirmFn = self.payload.type === 'tongue' ? confirmTongue : confirmReportPreview
  421. confirmFn(postData).then(function(res) {
  422. uni.hideLoading()
  423. if (res.code === 200) {
  424. uni.showToast({ title: '入库成功', icon: 'success' })
  425. var reportId = res.data && res.data.reportId
  426. var reportType = res.data && res.data.reportType || ''
  427. setTimeout(function() {
  428. if (reportId) {
  429. if (reportType === 'gut_flora') {
  430. uni.redirectTo({ url: '/pages/health/report-survey?reportId=' + reportId + '&childId=' + (postData.subjectId || '') })
  431. } else {
  432. uni.redirectTo({ url: '/pages/body/health-report?reportId=' + reportId })
  433. }
  434. } else {
  435. uni.navigateBack()
  436. }
  437. }, 500)
  438. } else {
  439. uni.showToast({ title: res.message || '确认失败', icon: 'none' })
  440. }
  441. }).catch(function(e) {
  442. uni.hideLoading()
  443. uni.showToast({ title: '确认失败,请重试', icon: 'none' })
  444. console.log('确认入库失败', e)
  445. }).finally(function() {
  446. self.confirming = false
  447. })
  448. },
  449. doDiscard() {
  450. var self = this
  451. uni.showModal({
  452. title: '确认放弃',
  453. content: '放弃后解析数据将被删除,需要重新上传',
  454. success: function(res) {
  455. if (res.confirm) {
  456. var discardFn = self.payload.type === 'tongue' ? discardTongue : discardReportDraft
  457. discardFn({ draftId: self.draftId }).then(function() {
  458. uni.showToast({ title: '已放弃', icon: 'none' })
  459. setTimeout(function() { uni.navigateBack() }, 500)
  460. }).catch(function() {
  461. uni.showToast({ title: '操作失败', icon: 'none' })
  462. })
  463. }
  464. }
  465. })
  466. },
  467. goBack() {
  468. uni.navigateBack()
  469. },
  470. // 舌诊指标标签映射
  471. indicatorLabel(code) {
  472. var map = {
  473. 'tongue_color': '舌色',
  474. 'coating_color': '苔色',
  475. 'coating_texture': '苔质',
  476. 'fissure': '裂纹',
  477. 'teeth_mark': '齿痕',
  478. 'sublingual_vein': '舌下静脉',
  479. 'constitution': '体质'
  480. }
  481. return map[code] || code
  482. }
  483. }
  484. }
  485. </script>
  486. <style scoped>
  487. .confirm-container {
  488. min-height: 100vh;
  489. background: #F5F7FA;
  490. }
  491. .nav-bar {
  492. position: sticky;
  493. top: 0;
  494. background: #fff;
  495. display: flex;
  496. flex-direction: row;
  497. align-items: center;
  498. justify-content: space-between;
  499. padding: 20rpx 30rpx;
  500. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
  501. z-index: 100;
  502. }
  503. .nav-back { font-size: 32rpx; color: #5B9BD5; }
  504. .nav-title { font-size: 32rpx; font-weight: bold; color: #333; }
  505. .nav-placeholder { width: 80rpx; }
  506. .tip-banner {
  507. display: flex;
  508. flex-direction: row;
  509. align-items: center;
  510. background: #E8F5E9;
  511. margin: 20rpx 30rpx;
  512. padding: 20rpx 24rpx;
  513. border-radius: 16rpx;
  514. }
  515. .tip-icon { font-size: 32rpx; margin-right: 16rpx; flex-shrink: 0; }
  516. .tip-text { font-size: 24rpx; color: #2E7D32; line-height: 1.5; }
  517. .form-section { margin: 0 30rpx; }
  518. .card {
  519. background: #fff;
  520. border-radius: 16rpx;
  521. padding: 24rpx;
  522. margin-bottom: 20rpx;
  523. box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.04);
  524. }
  525. .card-title {
  526. font-size: 28rpx;
  527. font-weight: bold;
  528. color: #333;
  529. margin-bottom: 20rpx;
  530. padding-bottom: 12rpx;
  531. border-bottom: 2rpx solid #F0F0F0;
  532. }
  533. .field-row {
  534. display: flex;
  535. flex-direction: row;
  536. align-items: center;
  537. margin-bottom: 16rpx;
  538. }
  539. .field-label {
  540. font-size: 26rpx;
  541. color: #666;
  542. width: 160rpx;
  543. flex-shrink: 0;
  544. }
  545. .field-value {
  546. font-size: 26rpx;
  547. color: #333;
  548. font-weight: 500;
  549. flex: 1;
  550. }
  551. .field-value.binding-required {
  552. color: #E65100;
  553. }
  554. .field-input {
  555. flex: 1;
  556. font-size: 26rpx;
  557. color: #333;
  558. padding: 8rpx 16rpx;
  559. background: #F5F7FA;
  560. border-radius: 8rpx;
  561. }
  562. .field-textarea {
  563. width: 100%;
  564. min-height: 120rpx;
  565. font-size: 26rpx;
  566. color: #333;
  567. padding: 16rpx;
  568. background: #F5F7FA;
  569. border-radius: 12rpx;
  570. box-sizing: border-box;
  571. margin-top: 8rpx;
  572. }
  573. .member-list {
  574. display: flex;
  575. flex-direction: row;
  576. flex-wrap: wrap;
  577. gap: 16rpx;
  578. }
  579. .member-item {
  580. display: flex;
  581. flex-direction: column;
  582. align-items: center;
  583. padding: 20rpx 24rpx;
  584. background: #F5F7FA;
  585. border-radius: 16rpx;
  586. border: 2rpx solid transparent;
  587. min-width: 120rpx;
  588. }
  589. .member-selected {
  590. background: #E3F2FD;
  591. border-color: #5B9BD5;
  592. }
  593. .member-avatar {
  594. width: 64rpx;
  595. height: 64rpx;
  596. border-radius: 50%;
  597. background: #5B9BD5;
  598. color: #fff;
  599. font-size: 28rpx;
  600. font-weight: bold;
  601. text-align: center;
  602. line-height: 64rpx;
  603. margin-bottom: 8rpx;
  604. }
  605. .member-name { font-size: 24rpx; color: #333; }
  606. .indicator-item {
  607. background: #F8FAFB;
  608. border-radius: 12rpx;
  609. padding: 16rpx;
  610. margin-bottom: 12rpx;
  611. }
  612. .ind-header {
  613. display: flex;
  614. flex-direction: row;
  615. align-items: center;
  616. gap: 12rpx;
  617. }
  618. .ind-name {
  619. flex: 1;
  620. font-size: 26rpx;
  621. color: #333;
  622. font-weight: 500;
  623. padding: 4rpx 8rpx;
  624. background: #fff;
  625. border-radius: 6rpx;
  626. }
  627. .ind-value {
  628. width: 160rpx;
  629. font-size: 26rpx;
  630. color: #1565C0;
  631. font-weight: bold;
  632. text-align: right;
  633. padding: 4rpx 8rpx;
  634. background: #fff;
  635. border-radius: 6rpx;
  636. }
  637. .ind-detail {
  638. display: flex;
  639. flex-direction: row;
  640. margin-top: 8rpx;
  641. gap: 16rpx;
  642. }
  643. .ind-ref { font-size: 22rpx; color: #94A3B8; }
  644. .ind-unit { font-size: 22rpx; color: #64748B; }
  645. .ind-symptoms {
  646. display: flex;
  647. flex-direction: row;
  648. align-items: center;
  649. margin-top: 8rpx;
  650. gap: 8rpx;
  651. }
  652. .ind-symptom-label { font-size: 22rpx; color: #94A3B8; flex-shrink: 0; }
  653. .ind-symptom-input {
  654. flex: 1;
  655. font-size: 22rpx;
  656. color: #333;
  657. padding: 4rpx 8rpx;
  658. background: #fff;
  659. border-radius: 6rpx;
  660. }
  661. .ind-status-row { margin-top: 8rpx; }
  662. .status-tag {
  663. display: inline-block;
  664. font-size: 20rpx;
  665. padding: 4rpx 12rpx;
  666. border-radius: 8rpx;
  667. font-weight: 500;
  668. }
  669. .status-normal { background: #E8F5E9; color: #2E7D32; }
  670. .status-high { background: #FFF3E0; color: #E65100; }
  671. .status-low { background: #E3F2FD; color: #1565C0; }
  672. .status-abnormal { background: #FFEBEE; color: #C62828; }
  673. .flora-item {
  674. display: flex;
  675. flex-direction: row;
  676. align-items: center;
  677. gap: 12rpx;
  678. padding: 12rpx 16rpx;
  679. background: #F8FAFB;
  680. border-radius: 10rpx;
  681. margin-bottom: 8rpx;
  682. }
  683. .flora-name {
  684. flex: 1;
  685. font-size: 26rpx;
  686. color: #333;
  687. padding: 4rpx 8rpx;
  688. background: #fff;
  689. border-radius: 6rpx;
  690. }
  691. .flora-abundance {
  692. width: 140rpx;
  693. font-size: 26rpx;
  694. color: #1565C0;
  695. text-align: right;
  696. padding: 4rpx 8rpx;
  697. background: #fff;
  698. border-radius: 6rpx;
  699. }
  700. .risk-item {
  701. display: flex;
  702. flex-direction: row;
  703. align-items: center;
  704. gap: 12rpx;
  705. padding: 12rpx 16rpx;
  706. background: #F8FAFB;
  707. border-radius: 10rpx;
  708. margin-bottom: 8rpx;
  709. }
  710. .risk-name {
  711. flex: 1;
  712. font-size: 26rpx;
  713. color: #333;
  714. padding: 4rpx 8rpx;
  715. background: #fff;
  716. border-radius: 6rpx;
  717. }
  718. .risk-level {
  719. font-size: 22rpx;
  720. font-weight: 500;
  721. padding: 4rpx 12rpx;
  722. border-radius: 8rpx;
  723. }
  724. .risk-low { background: #E8F5E9; color: #2E7D32; }
  725. .risk-medium, .risk-moderate { background: #FFF3E0; color: #E65100; }
  726. .risk-high { background: #FFEBEE; color: #C62828; }
  727. .food-item {
  728. display: flex;
  729. flex-direction: row;
  730. align-items: center;
  731. gap: 12rpx;
  732. padding: 12rpx 16rpx;
  733. background: #F8FAFB;
  734. border-radius: 10rpx;
  735. margin-bottom: 8rpx;
  736. }
  737. .food-name {
  738. flex: 1;
  739. font-size: 26rpx;
  740. color: #333;
  741. padding: 4rpx 8rpx;
  742. background: #fff;
  743. border-radius: 6rpx;
  744. }
  745. .food-category {
  746. width: 160rpx;
  747. font-size: 24rpx;
  748. color: #64748B;
  749. text-align: right;
  750. padding: 4rpx 8rpx;
  751. background: #fff;
  752. border-radius: 6rpx;
  753. }
  754. .action-row {
  755. display: flex;
  756. flex-direction: row;
  757. gap: 20rpx;
  758. margin-top: 10rpx;
  759. margin-bottom: 20rpx;
  760. }
  761. .btn-discard {
  762. flex: 1;
  763. background: #fff;
  764. border: 2rpx solid #EF4444;
  765. color: #EF4444;
  766. font-size: 28rpx;
  767. font-weight: bold;
  768. text-align: center;
  769. padding: 24rpx;
  770. border-radius: 16rpx;
  771. }
  772. .btn-discard:active { opacity: 0.7; }
  773. .btn-confirm {
  774. flex: 2;
  775. background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
  776. color: #fff;
  777. font-size: 28rpx;
  778. font-weight: bold;
  779. text-align: center;
  780. padding: 24rpx;
  781. border-radius: 16rpx;
  782. }
  783. .btn-confirm:active { opacity: 0.85; }
  784. .btn-disabled { opacity: 0.5; }
  785. .bottom-spacer { height: 80rpx; }
  786. /* ===== Phase 2: 匹配可信度 ===== */
  787. .confidence-card {
  788. margin: 0 30rpx 20rpx;
  789. background: #fff;
  790. border-radius: 16rpx;
  791. padding: 24rpx;
  792. box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.04);
  793. }
  794. .confidence-header {
  795. display: flex;
  796. flex-direction: row;
  797. align-items: center;
  798. justify-content: space-between;
  799. margin-bottom: 16rpx;
  800. }
  801. .confidence-label {
  802. font-size: 26rpx;
  803. color: #333;
  804. font-weight: 500;
  805. }
  806. .confidence-badge {
  807. font-size: 22rpx;
  808. font-weight: bold;
  809. padding: 4rpx 16rpx;
  810. border-radius: 20rpx;
  811. }
  812. .badge-high { background: #E8F5E9; color: #2E7D32; }
  813. .badge-medium { background: #FFF8E1; color: #F57F17; }
  814. .badge-low { background: #FFF3E0; color: #E65100; }
  815. .badge-none { background: #FFEBEE; color: #C62828; }
  816. .confidence-bar-track {
  817. height: 12rpx;
  818. background: #E2E8F0;
  819. border-radius: 6rpx;
  820. overflow: hidden;
  821. margin-bottom: 12rpx;
  822. }
  823. .confidence-bar-fill {
  824. height: 100%;
  825. border-radius: 6rpx;
  826. transition: width 0.3s ease;
  827. }
  828. .fill-high { background: linear-gradient(90deg, #66BB6A, #43A047); }
  829. .fill-medium { background: linear-gradient(90deg, #FFD54F, #FFC107); }
  830. .fill-low { background: linear-gradient(90deg, #FF8A65, #FF5722); }
  831. .fill-none { background: linear-gradient(90deg, #EF9A9A, #E53935); }
  832. .confidence-desc {
  833. font-size: 22rpx;
  834. color: #94A3B8;
  835. line-height: 1.4;
  836. }
  837. /* ===== Phase 2: 候选成员列表 ===== */
  838. .candidate-section {
  839. margin-bottom: 16rpx;
  840. }
  841. .candidate-section-hint {
  842. display: block;
  843. font-size: 24rpx;
  844. color: #64748B;
  845. margin-bottom: 12rpx;
  846. }
  847. .candidate-item {
  848. display: flex;
  849. flex-direction: row;
  850. align-items: center;
  851. justify-content: space-between;
  852. padding: 16rpx;
  853. background: #F8FAFB;
  854. border-radius: 12rpx;
  855. margin-bottom: 10rpx;
  856. border: 2rpx solid transparent;
  857. }
  858. .candidate-item:active { opacity: 0.8; }
  859. .candidate-selected {
  860. background: #E3F2FD;
  861. border-color: #5B9BD5;
  862. }
  863. .candidate-left {
  864. display: flex;
  865. flex-direction: row;
  866. align-items: center;
  867. flex: 1;
  868. min-width: 0;
  869. }
  870. .candidate-avatar {
  871. width: 56rpx;
  872. height: 56rpx;
  873. border-radius: 50%;
  874. background: #5B9BD5;
  875. color: #fff;
  876. font-size: 24rpx;
  877. font-weight: bold;
  878. text-align: center;
  879. line-height: 56rpx;
  880. margin-right: 16rpx;
  881. flex-shrink: 0;
  882. }
  883. .candidate-info {
  884. flex: 1;
  885. min-width: 0;
  886. }
  887. .candidate-name {
  888. font-size: 26rpx;
  889. color: #333;
  890. font-weight: 500;
  891. display: block;
  892. }
  893. .candidate-detail {
  894. font-size: 22rpx;
  895. color: #94A3B8;
  896. display: block;
  897. }
  898. .candidate-fields {
  899. font-size: 20rpx;
  900. color: #5B9BD5;
  901. display: block;
  902. margin-top: 2rpx;
  903. }
  904. .candidate-right {
  905. display: flex;
  906. flex-direction: row;
  907. align-items: center;
  908. gap: 12rpx;
  909. flex-shrink: 0;
  910. margin-left: 16rpx;
  911. }
  912. .score-bar-track {
  913. width: 80rpx;
  914. height: 10rpx;
  915. background: #E2E8F0;
  916. border-radius: 5rpx;
  917. overflow: hidden;
  918. }
  919. .score-bar-fill {
  920. height: 100%;
  921. background: linear-gradient(90deg, #5B9BD5, #3A7CC4);
  922. border-radius: 5rpx;
  923. }
  924. .score-text {
  925. font-size: 24rpx;
  926. color: #1565C0;
  927. font-weight: bold;
  928. min-width: 48rpx;
  929. text-align: right;
  930. }
  931. /* ===== Phase 2: 新建成员入口 ===== */
  932. .create-member-entry {
  933. display: flex;
  934. flex-direction: row;
  935. align-items: center;
  936. justify-content: center;
  937. padding: 20rpx;
  938. border: 2rpx dashed #CBD5E1;
  939. border-radius: 12rpx;
  940. margin-top: 8rpx;
  941. }
  942. .create-member-entry:active { background: #F1F5F9; }
  943. .create-member-icon {
  944. font-size: 28rpx;
  945. color: #5B9BD5;
  946. font-weight: bold;
  947. margin-right: 8rpx;
  948. }
  949. .create-member-text {
  950. font-size: 24rpx;
  951. color: #5B9BD5;
  952. }
  953. /* ===== Phase 2: 新建成员表单 ===== */
  954. .auto-create-card {
  955. border: 2rpx solid #E3F2FD;
  956. }
  957. .form-item {
  958. display: flex;
  959. flex-direction: row;
  960. align-items: center;
  961. margin-bottom: 20rpx;
  962. }
  963. .form-item-label {
  964. font-size: 26rpx;
  965. color: #333;
  966. width: 100rpx;
  967. flex-shrink: 0;
  968. }
  969. .form-item-input {
  970. flex: 1;
  971. font-size: 26rpx;
  972. color: #333;
  973. padding: 12rpx 16rpx;
  974. background: #F5F7FA;
  975. border-radius: 10rpx;
  976. }
  977. .gender-selector {
  978. display: flex;
  979. flex-direction: row;
  980. gap: 16rpx;
  981. }
  982. .gender-option {
  983. font-size: 26rpx;
  984. color: #333;
  985. padding: 12rpx 32rpx;
  986. background: #F5F7FA;
  987. border-radius: 10rpx;
  988. border: 2rpx solid transparent;
  989. }
  990. .gender-option:active { opacity: 0.7; }
  991. .gender-selected {
  992. background: #E3F2FD;
  993. border-color: #5B9BD5;
  994. color: #1565C0;
  995. }
  996. .btn-create-submit {
  997. background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
  998. color: #fff;
  999. font-size: 28rpx;
  1000. font-weight: bold;
  1001. text-align: center;
  1002. padding: 22rpx;
  1003. border-radius: 14rpx;
  1004. margin-top: 10rpx;
  1005. }
  1006. .btn-create-submit:active { opacity: 0.85; }
  1007. /* ===== 舌诊指标 ===== */
  1008. .tongue-indicator-grid {
  1009. display: flex;
  1010. flex-direction: row;
  1011. flex-wrap: wrap;
  1012. gap: 12rpx;
  1013. }
  1014. .tongue-ind-item {
  1015. width: calc(50% - 6rpx);
  1016. background: #F8FAFC;
  1017. border-radius: 16rpx;
  1018. padding: 20rpx 24rpx;
  1019. box-sizing: border-box;
  1020. }
  1021. .tongue-ind-label {
  1022. display: block;
  1023. font-size: 24rpx;
  1024. color: #94A3B8;
  1025. margin-bottom: 8rpx;
  1026. }
  1027. .tongue-ind-value {
  1028. display: block;
  1029. font-size: 28rpx;
  1030. color: #1E293B;
  1031. font-weight: 500;
  1032. }
  1033. .tongue-summary {
  1034. margin-top: 20rpx;
  1035. padding: 20rpx 24rpx;
  1036. background: #FFF8E1;
  1037. border-radius: 16rpx;
  1038. }
  1039. .tongue-summary-title {
  1040. font-size: 24rpx;
  1041. color: #F59E0B;
  1042. font-weight: 600;
  1043. display: block;
  1044. margin-bottom: 8rpx;
  1045. }
  1046. .tongue-summary-text {
  1047. font-size: 26rpx;
  1048. color: #333;
  1049. line-height: 1.6;
  1050. }
  1051. </style>