MedicalHistoryStep.vue 15 KB

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