MedicalHistoryStep.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <template>
  2. <view class="medical-step">
  3. <view class="step-title-row">
  4. <text class="step-title">疾病史与用药</text>
  5. <text class="step-sub">选择过往疾病与当前用药,如无请勾选「无长期用药」</text>
  6. </view>
  7. <!-- 疾病史 -->
  8. <view class="form-card">
  9. <view class="card-head">
  10. <text class="card-title">疾病史</text>
  11. <text class="card-count">已选 {{ inner.selectedDiseases.length }} 项</text>
  12. </view>
  13. <view v-for="(group, gi) in diseaseGroups" :key="group.key" class="disease-group">
  14. <view class="group-head" @click="toggleGroup(group.key)">
  15. <text class="group-title">{{ group.title }}</text>
  16. <text class="group-arrow">{{ expandedGroups.indexOf(group.key) >= 0 ? '收起' : '展开' }}</text>
  17. </view>
  18. <view v-if="expandedGroups.indexOf(group.key) >= 0" class="tag-wrap">
  19. <view
  20. v-for="(item, ii) in group.items"
  21. :key="item.name"
  22. class="tag-item"
  23. :class="{ 'tag-active': selectedNames.indexOf(item.name) >= 0 }"
  24. @click="toggleDisease(item.name)"
  25. >
  26. <text>{{ item.name }}</text>
  27. <text v-if="item.risk" class="risk-badge">★</text>
  28. </view>
  29. </view>
  30. </view>
  31. <!-- 自定义疾病 -->
  32. <view class="custom-row">
  33. <input class="custom-input" type="text" v-model="diseaseInput" placeholder="输入其他疾病" />
  34. <text class="add-link" @click="addCustomDisease">添加</text>
  35. </view>
  36. <!-- 已选疾病 + 确诊时间 -->
  37. <view v-if="inner.selectedDiseases.length > 0" class="selected-area">
  38. <view class="selected-title">已选疾病</view>
  39. <view v-for="(item, si) in inner.selectedDiseases" :key="getSelKey(si)" class="sel-row">
  40. <text class="sel-name">{{ item.name }}</text>
  41. <picker mode="date" fields="month" :value="item.diagnosedAt || currentMonth" :end="monthEnd" @change="onDiagChange(si, $event)">
  42. <view class="sel-diag">{{ item.diagnosedAt ? item.diagnosedAt : '设确诊时间' }}</view>
  43. </picker>
  44. <text class="sel-del" @click="removeSelected(si)">×</text>
  45. </view>
  46. </view>
  47. </view>
  48. <!-- 在服药物/治疗 -->
  49. <view class="form-card">
  50. <view class="card-head">
  51. <text class="card-title">在服药物/治疗</text>
  52. <text class="card-count">{{ inner.medRows.length }} 条</text>
  53. </view>
  54. <view v-for="(med, idx) in inner.medRows" :key="getMedKey(idx)" class="med-row">
  55. <view class="med-line">
  56. <input class="med-name" type="text" v-model="med.name" placeholder="药物品牌,如 立普妥"
  57. @input="emitInput" />
  58. <text class="med-del" @click="removeMed(idx)">删除</text>
  59. </view>
  60. <view class="med-line">
  61. <picker mode="date" fields="month" :value="med.startDate || currentMonth" :end="monthEnd" @change="onMedStartChange(idx, $event)">
  62. <view class="med-picker">{{ med.startDate ? med.startDate : '选择开始日期' }}</view>
  63. </picker>
  64. <text class="med-forget" @click="onForgetClick(idx)">记不清</text>
  65. </view>
  66. <view class="med-line" v-if="med.durationText">
  67. <text class="med-duration">已服时长:{{ med.durationText }}</text>
  68. </view>
  69. <view class="med-line">
  70. <input class="med-time" type="text" v-model="med.time" placeholder="服药时间,如 早饭后一次"
  71. @input="emitInput" />
  72. </view>
  73. </view>
  74. <text class="add-link" @click="addMed">+ 添加药物/治疗</text>
  75. </view>
  76. <!-- 快捷:无长期用药 -->
  77. <view class="form-card">
  78. <view
  79. class="quick-chip"
  80. :class="{ 'quick-chip-on': noMedication }"
  81. @click="toggleNoMedication"
  82. >
  83. <text>{{ noMedication ? '✓ 已选' : '无长期用药' }}</text>
  84. </view>
  85. <text class="unit-hint" v-if="noMedication">已标记无长期用药,可跳过药物填写</text>
  86. </view>
  87. </view>
  88. </template>
  89. <script>
  90. // 预置疾病清单(与 health-plan-summary.vue 保持一致的 8 组)
  91. var DISEASE_GROUPS = [
  92. { key: 'cardio', title: '心血管代谢', items: [
  93. { name: '高血压', risk: false },
  94. { name: '糖尿病', risk: false },
  95. { name: '血脂异常', risk: false },
  96. { name: '冠心病', risk: false },
  97. { name: '脑卒中', risk: false },
  98. { name: '心力衰竭', risk: false },
  99. { name: '心房颤动', risk: false },
  100. { name: '慢阻肺', risk: false },
  101. { name: '心脑血管疾病', risk: true },
  102. { name: 'II型糖尿病', risk: true },
  103. { name: '肥胖', risk: true },
  104. { name: '超重', risk: false },
  105. { name: '高尿酸血症', risk: false },
  106. { name: '骨质疏松', risk: false }
  107. ]},
  108. { key: 'digest', title: '消化系统', items: [
  109. { name: '幽门螺杆菌感染', risk: false },
  110. { name: '脂肪肝', risk: false },
  111. { name: '慢性胃炎', risk: false },
  112. { name: '胃食管反流', risk: false },
  113. { name: '肠易激综合征', risk: true },
  114. { name: '炎症性肠炎', risk: true },
  115. { name: '慢性便秘', risk: true },
  116. { name: '感染性腹泻', risk: true },
  117. { name: '胆结石', risk: false },
  118. { name: '炎症性肠病', risk: false },
  119. { name: '肝病', risk: true },
  120. { name: '胃病', risk: true },
  121. { name: '胆病', risk: true },
  122. { name: '肾病', risk: true },
  123. { name: '腹胀', risk: true }
  124. ]},
  125. { key: 'immune', title: '过敏免疫', items: [
  126. { name: '过敏性鼻炎', risk: true },
  127. { name: '哮喘', risk: false },
  128. { name: '湿疹', risk: false },
  129. { name: '食物过敏', risk: false },
  130. { name: '自体免疫病', risk: true }
  131. ]},
  132. { key: 'neuro', title: '精神神经', items: [
  133. { name: '抑郁症', risk: true },
  134. { name: '失眠', risk: true },
  135. { name: '焦虑障碍', risk: false },
  136. { name: '偏头痛', risk: false },
  137. { name: '自闭症', risk: true }
  138. ]},
  139. { key: 'resp', title: '呼吸系统', items: [
  140. { name: '肺部感染或疾病', risk: true },
  141. { name: '肺部疾病', risk: true }
  142. ]},
  143. { key: 'tumor', title: '肿瘤', items: [
  144. { name: '结直肠癌', risk: true },
  145. { name: '肺癌', risk: false },
  146. { name: '甲状腺癌', risk: false },
  147. { name: '肝癌', risk: false },
  148. { name: '胃癌', risk: false },
  149. { name: '乳腺癌', risk: false },
  150. { name: '食管癌', risk: false }
  151. ]},
  152. { key: 'child', title: '儿童青少年', items: [
  153. { name: '近视', risk: false },
  154. { name: '龋齿', risk: false },
  155. { name: '注意缺陷多动障碍', risk: false },
  156. { name: '甲状腺疾病', risk: true }
  157. ]},
  158. { key: 'other', title: '其他', items: [
  159. { name: '缺铁性贫血', risk: false },
  160. { name: '贫血', risk: false }
  161. ]}
  162. ]
  163. export default {
  164. name: 'MedicalHistoryStep',
  165. props: {
  166. value: {
  167. type: Object,
  168. default: function() { return {} }
  169. }
  170. },
  171. data() {
  172. return {
  173. diseaseGroups: DISEASE_GROUPS,
  174. expandedGroups: [],
  175. diseaseInput: '',
  176. currentMonth: '',
  177. monthEnd: ''
  178. }
  179. },
  180. computed: {
  181. inner: function() {
  182. return this.value || {}
  183. },
  184. noMedication: function() {
  185. return !!this.inner.noMedication
  186. },
  187. selectedNames: function() {
  188. var list = this.inner.selectedDiseases || []
  189. return list.map(function(d) { return d.name })
  190. }
  191. },
  192. created: function() {
  193. var now = new Date()
  194. var y = now.getFullYear()
  195. var m = now.getMonth() + 1
  196. var mStr = m < 10 ? '0' + m : '' + m
  197. this.currentMonth = y + '-' + mStr
  198. var lastDay = new Date(y, m, 0).getDate()
  199. this.monthEnd = y + '-' + mStr + '-' + (lastDay < 10 ? '0' + lastDay : '' + lastDay)
  200. // 默认展开前 3 组
  201. this.expandedGroups = DISEASE_GROUPS.slice(0, 3).map(function(g) { return g.key })
  202. // 已有选中项时展开对应组
  203. var self = this
  204. ;(this.inner.selectedDiseases || []).forEach(function(d) {
  205. var gk = self.findGroupKey(d.name)
  206. if (gk && self.expandedGroups.indexOf(gk) < 0) self.expandedGroups.push(gk)
  207. })
  208. },
  209. methods: {
  210. emitInput: function() {
  211. this.$emit('input', this.inner)
  212. },
  213. findGroupKey: function(name) {
  214. for (var i = 0; i < DISEASE_GROUPS.length; i++) {
  215. var items = DISEASE_GROUPS[i].items
  216. for (var j = 0; j < items.length; j++) {
  217. if (items[j].name === name) return DISEASE_GROUPS[i].key
  218. }
  219. }
  220. return ''
  221. },
  222. toggleGroup: function(key) {
  223. var idx = this.expandedGroups.indexOf(key)
  224. if (idx >= 0) this.expandedGroups.splice(idx, 1)
  225. else this.expandedGroups.push(key)
  226. },
  227. toggleDisease: function(name) {
  228. var list = this.inner.selectedDiseases || []
  229. var idx = -1
  230. for (var i = 0; i < list.length; i++) {
  231. if (list[i].name === name) { idx = i; break }
  232. }
  233. var next
  234. if (idx >= 0) {
  235. next = list.slice(0, idx).concat(list.slice(idx + 1))
  236. } else {
  237. next = list.concat([{ name: name, diagnosedAt: '', custom: false }])
  238. }
  239. this.$emit('input', Object.assign({}, this.inner, { selectedDiseases: next }))
  240. },
  241. addCustomDisease: function() {
  242. var name = (this.diseaseInput || '').trim()
  243. if (!name) return
  244. var list = this.inner.selectedDiseases || []
  245. var exists = list.some(function(d) { return d.name === name })
  246. if (!exists) {
  247. var next = list.concat([{ name: name, diagnosedAt: '', custom: true }])
  248. this.$emit('input', Object.assign({}, this.inner, { selectedDiseases: next }))
  249. }
  250. this.diseaseInput = ''
  251. },
  252. getSelKey: function(i) { return 's' + i },
  253. getMedKey: function(i) { return 'm' + i },
  254. removeSelected: function(idx) {
  255. var list = this.inner.selectedDiseases || []
  256. var next = list.slice(0, idx).concat(list.slice(idx + 1))
  257. this.$emit('input', Object.assign({}, this.inner, { selectedDiseases: next }))
  258. },
  259. onDiagChange: function(idx, e) {
  260. var list = this.inner.selectedDiseases || []
  261. var item = Object.assign({}, list[idx], { diagnosedAt: e.detail.value })
  262. var next = list.slice()
  263. next[idx] = item
  264. this.$emit('input', Object.assign({}, this.inner, { selectedDiseases: next }))
  265. },
  266. addMed: function() {
  267. var rows = (this.inner.medRows && this.inner.medRows.length > 0 ? this.inner.medRows.slice() : [])
  268. rows.push({ name: '', startDate: '', durationText: '', time: '' })
  269. this.$emit('input', Object.assign({}, this.inner, { medRows: rows }))
  270. },
  271. removeMed: function(idx) {
  272. var rows = (this.inner.medRows || []).slice()
  273. rows.splice(idx, 1)
  274. this.$emit('input', Object.assign({}, this.inner, { medRows: rows }))
  275. },
  276. onMedStartChange: function(idx, e) {
  277. var start = e.detail.value
  278. var rows = (this.inner.medRows || []).slice()
  279. rows[idx] = Object.assign({}, rows[idx], { startDate: start, durationText: this.calcDuration(start) })
  280. this.$emit('input', Object.assign({}, this.inner, { medRows: rows }))
  281. },
  282. onForgetClick: function(idx) {
  283. var self = this
  284. uni.showModal({
  285. title: '记不清开始日期',
  286. editable: true,
  287. placeholderText: '如:约 3 个月',
  288. success: function(res) {
  289. if (res.confirm && res.content) {
  290. var rows = (self.inner.medRows || []).slice()
  291. rows[idx] = Object.assign({}, rows[idx], { durationText: res.content.trim() })
  292. self.$emit('input', Object.assign({}, self.inner, { medRows: rows }))
  293. }
  294. }
  295. })
  296. },
  297. calcDuration: function(startDate) {
  298. if (!startDate) return ''
  299. var parts = startDate.split('-')
  300. if (parts.length !== 2) return ''
  301. var sy = parseInt(parts[0], 10)
  302. var sm = parseInt(parts[1], 10)
  303. var now = new Date()
  304. var months = (now.getFullYear() - sy) * 12 + (now.getMonth() + 1 - sm)
  305. if (months < 1) months = 1
  306. if (months < 12) return '已服 ' + months + ' 个月'
  307. return '已服 ' + Math.floor(months / 12) + ' 年'
  308. },
  309. toggleNoMedication: function() {
  310. this.$emit('input', Object.assign({}, this.inner, { noMedication: !this.noMedication }))
  311. }
  312. }
  313. }
  314. </script>
  315. <style scoped>
  316. .medical-step {
  317. padding: 10rpx 0 30rpx;
  318. }
  319. .step-title-row {
  320. margin-bottom: 20rpx;
  321. }
  322. .step-title {
  323. font-size: 34rpx;
  324. font-weight: 700;
  325. color: #333;
  326. }
  327. .step-sub {
  328. display: block;
  329. font-size: 24rpx;
  330. color: #999;
  331. margin-top: 8rpx;
  332. }
  333. .form-card {
  334. background: #fff;
  335. border-radius: 20rpx;
  336. padding: 24rpx;
  337. margin-bottom: 20rpx;
  338. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  339. }
  340. .card-head {
  341. display: flex;
  342. align-items: center;
  343. justify-content: space-between;
  344. margin-bottom: 16rpx;
  345. }
  346. .card-title {
  347. font-size: 28rpx;
  348. font-weight: 600;
  349. color: #333;
  350. }
  351. .card-count {
  352. font-size: 22rpx;
  353. color: #F97316;
  354. background: #FFF3E0;
  355. padding: 4rpx 16rpx;
  356. border-radius: 20rpx;
  357. }
  358. .group-head {
  359. display: flex;
  360. align-items: center;
  361. justify-content: space-between;
  362. padding: 16rpx 0 8rpx;
  363. }
  364. .group-title {
  365. font-size: 26rpx;
  366. font-weight: 600;
  367. color: #475569;
  368. }
  369. .group-arrow {
  370. font-size: 22rpx;
  371. color: #94A3B8;
  372. }
  373. .tag-wrap {
  374. display: flex;
  375. flex-wrap: wrap;
  376. }
  377. .tag-item {
  378. padding: 12rpx 24rpx;
  379. border-radius: 999rpx;
  380. background: #F1F5F9;
  381. font-size: 24rpx;
  382. color: #334155;
  383. margin: 0 14rpx 14rpx 0;
  384. position: relative;
  385. }
  386. .tag-active {
  387. background: #FFF3E0;
  388. color: #F97316;
  389. font-weight: 600;
  390. }
  391. .risk-badge {
  392. color: #EF4444;
  393. margin-left: 6rpx;
  394. }
  395. .custom-row {
  396. display: flex;
  397. align-items: center;
  398. margin-top: 10rpx;
  399. }
  400. .custom-input {
  401. flex: 1;
  402. border: 2rpx solid #E2E8F0;
  403. border-radius: 12rpx;
  404. padding: 14rpx 20rpx;
  405. font-size: 26rpx;
  406. background: #F8FAFC;
  407. }
  408. .add-link {
  409. color: #F97316;
  410. font-size: 26rpx;
  411. font-weight: 600;
  412. padding: 14rpx 10rpx;
  413. margin-left: 10rpx;
  414. }
  415. .selected-area {
  416. margin-top: 16rpx;
  417. border-top: 2rpx solid #F1F5F9;
  418. padding-top: 16rpx;
  419. }
  420. .selected-title {
  421. font-size: 24rpx;
  422. color: #94A3B8;
  423. margin-bottom: 10rpx;
  424. }
  425. .sel-row {
  426. display: flex;
  427. align-items: center;
  428. margin-bottom: 12rpx;
  429. }
  430. .sel-name {
  431. flex: 1;
  432. font-size: 26rpx;
  433. color: #333;
  434. }
  435. .sel-diag {
  436. font-size: 22rpx;
  437. color: #F97316;
  438. background: #FFF3E0;
  439. padding: 8rpx 20rpx;
  440. border-radius: 999rpx;
  441. }
  442. .sel-del {
  443. color: #EF4444;
  444. font-size: 32rpx;
  445. padding: 0 10rpx;
  446. }
  447. .med-row {
  448. border: 2rpx solid #F1F5F9;
  449. border-radius: 12rpx;
  450. padding: 16rpx;
  451. margin-bottom: 16rpx;
  452. }
  453. .med-line {
  454. display: flex;
  455. align-items: center;
  456. margin-bottom: 10rpx;
  457. }
  458. .med-line:last-child {
  459. margin-bottom: 0;
  460. }
  461. .med-name,
  462. .med-time {
  463. flex: 1;
  464. border: 2rpx solid #E2E8F0;
  465. border-radius: 10rpx;
  466. padding: 12rpx 16rpx;
  467. font-size: 26rpx;
  468. background: #F8FAFC;
  469. }
  470. .med-del {
  471. color: #EF4444;
  472. font-size: 24rpx;
  473. padding: 0 10rpx;
  474. }
  475. .med-picker {
  476. font-size: 24rpx;
  477. color: #F97316;
  478. background: #FFF3E0;
  479. padding: 10rpx 20rpx;
  480. border-radius: 999rpx;
  481. }
  482. .med-forget {
  483. font-size: 22rpx;
  484. color: #94A3B8;
  485. margin-left: 16rpx;
  486. }
  487. .med-duration {
  488. font-size: 22rpx;
  489. color: #64748B;
  490. }
  491. .quick-chip {
  492. display: inline-flex;
  493. align-items: center;
  494. padding: 14rpx 28rpx;
  495. border-radius: 999rpx;
  496. background: #F1F5F9;
  497. font-size: 26rpx;
  498. color: #64748B;
  499. }
  500. .quick-chip-on {
  501. background: #FFF3E0;
  502. color: #F97316;
  503. font-weight: 600;
  504. }
  505. .unit-hint {
  506. display: block;
  507. font-size: 22rpx;
  508. color: #999;
  509. margin-top: 10rpx;
  510. }
  511. </style>