西藏巴青项目

index.vue 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. <template>
  2. <!-- 用药工具:顶区 up-tabs;下为三个 Tab 表单与结果区(文案 i18n),白底 -->
  3. <view :class="pageRootClass" class="tab-page mt-page">
  4. <scroll-view scroll-y class="mt-scroll" enable-back-to-top :show-scrollbar="false">
  5. <view class="mt-tabs-wrap">
  6. <up-tabs
  7. :current="tabIndex"
  8. class="mt-tabs"
  9. :list="tabsList"
  10. key-name="name"
  11. :scrollable="false"
  12. line-color="#22C55E"
  13. :active-style="{ color: '#15803d', fontWeight: '600' }"
  14. :inactive-style="{ color: '#78716c' }"
  15. @update:current="tabIndex = $event"
  16. />
  17. </view>
  18. <!-- 查询药物 -->
  19. <view v-show="tabIndex === 0" class="mt-panel">
  20. <view class="mt-row">
  21. <text class="mt-label text-body">{{ $t('medicineToolsPage.labelDrugName') }}</text>
  22. <up-search
  23. v-model="qDrugKeyword"
  24. class="mt-search"
  25. shape="round"
  26. :placeholder="$t('medicineToolsPage.searchDrugPlaceholder')"
  27. :show-action="false"
  28. :clearabled="true"
  29. bg-color="#f5f5f5"
  30. border-color="#e8e8e8"
  31. />
  32. </view>
  33. <view class="mt-row-btn">
  34. <up-button
  35. type="primary"
  36. shape="circle"
  37. :text="$t('medicineToolsPage.btnQuery')"
  38. :loading="queryLoading"
  39. @click="onQueryDrug"
  40. />
  41. </view>
  42. <view class="mt-gap-lg" />
  43. <text class="mt-line text-body">{{ $t('medicineToolsPage.labelDrugType') }}:{{ qDisplay.type }}</text>
  44. <text class="mt-line text-body">{{ $t('medicineToolsPage.labelWithdrawal') }}:{{ qDisplay.withdrawal }}</text>
  45. <text class="mt-line text-body mt-line--block">{{ $t('medicineToolsPage.labelGuide') }}:{{ qDisplay.guide }}</text>
  46. <text class="mt-line text-body mt-line--block">{{ $t('medicineToolsPage.labelTaboo') }}:{{ qDisplay.taboo }}</text>
  47. </view>
  48. <!-- 计算休药期 -->
  49. <view v-show="tabIndex === 1" class="mt-panel">
  50. <view class="mt-row">
  51. <text class="mt-label text-body">{{ $t('medicineToolsPage.labelDrugName') }}</text>
  52. <up-search
  53. v-model="wDrugKeyword"
  54. class="mt-search"
  55. shape="round"
  56. :placeholder="$t('medicineToolsPage.searchDrugPlaceholder')"
  57. :show-action="false"
  58. :clearabled="true"
  59. bg-color="#f5f5f5"
  60. border-color="#e8e8e8"
  61. />
  62. </view>
  63. <view class="mt-row">
  64. <text class="mt-label text-body">{{ $t('medicineToolsPage.labelStopDate') }}</text>
  65. <!-- 只读 picker:不用原生 disabled input,否则小程序上点击无法冒泡到父级,日历弹不出 -->
  66. <view class="mt-date-field" role="button" @tap.stop="openStopDateCalendar">
  67. <text
  68. class="mt-date-field__txt text-body"
  69. :class="{ 'mt-date-field__txt--placeholder': !stopDateStr }"
  70. >{{ stopDateStr || $t('medicineToolsPage.pickStopDate') }}</text>
  71. <up-icon name="arrow-right" color="#78716c" :size="18" />
  72. </view>
  73. </view>
  74. <view class="mt-row-btn">
  75. <up-button
  76. type="primary"
  77. shape="circle"
  78. :text="$t('medicineToolsPage.btnCalc')"
  79. :loading="calcLoading"
  80. @click="onCalcWithdrawal"
  81. />
  82. </view>
  83. <view class="mt-gap-lg" />
  84. <text class="mt-line text-body">{{ $t('medicineToolsPage.labelWithdrawal') }}:{{ wDisplay.withdrawal }}</text>
  85. <text class="mt-line text-body mt-line--block">{{ $t('medicineToolsPage.labelEndDate') }}:{{ wDisplay.endDate }}</text>
  86. </view>
  87. <!-- 用药配伍:默认 2 行,可添加至最多 20 行;至少保留 1 行 -->
  88. <view v-show="tabIndex === 2" class="mt-panel">
  89. <view v-for="(row, idx) in compatDrugRows" :key="row.id" class="mt-row mt-row--compat">
  90. <text class="mt-label text-body">{{ $t('medicineToolsPage.labelDrugNth', { n: idx + 1 }) }}</text>
  91. <up-search
  92. v-model="row.keyword"
  93. class="mt-search"
  94. shape="round"
  95. :placeholder="$t('medicineToolsPage.searchDrugPlaceholder')"
  96. :show-action="false"
  97. :clearabled="true"
  98. bg-color="#f5f5f5"
  99. border-color="#e8e8e8"
  100. />
  101. <view
  102. v-if="compatDrugRows.length > 2"
  103. class="mt-compat-remove"
  104. role="button"
  105. @tap="removeCompatDrugRow(row.id)"
  106. >
  107. <up-icon name="trash" color="#9ca3af" :size="20" />
  108. <text class="mt-compat-remove__txt text-body">{{ $t('medicineToolsPage.btnRemoveCompatRow') }}</text>
  109. </view>
  110. </view>
  111. <view class="mt-compat-add">
  112. <up-button
  113. type="success"
  114. plain
  115. hairline
  116. size="small"
  117. :text="$t('medicineToolsPage.btnAddCompatDrug')"
  118. :disabled="compatDrugRows.length >= compatMaxRows"
  119. @click="addCompatDrugRow"
  120. />
  121. </view>
  122. <view class="mt-row-btn">
  123. <up-button
  124. type="primary"
  125. shape="circle"
  126. :text="$t('medicineToolsPage.btnCompat')"
  127. :loading="compatLoading"
  128. @click="onCompat"
  129. />
  130. </view>
  131. <view class="mt-gap-lg" />
  132. <text class="mt-line text-body">{{ $t('medicineToolsPage.labelForbidden') }}:{{ cDisplay.forbidden }}</text>
  133. <text class="mt-line text-body mt-line--block">{{ $t('medicineToolsPage.labelCompatResult') }}:{{ cDisplay.result }}</text>
  134. </view>
  135. <view class="mt-footer-spacer" />
  136. </scroll-view>
  137. <up-calendar
  138. :key="calendarRenderKey"
  139. :show="calendarShow"
  140. mode="single"
  141. :month-switch="true"
  142. :min-date="calendarMinDate"
  143. :max-date="calendarMaxDate"
  144. :default-date="calendarDefaultDate"
  145. :month-num="calendarMonthNum"
  146. color="#22C55E"
  147. :round="16"
  148. :close-on-click-overlay="true"
  149. :title="$t('medicineToolsPage.calendarTitle')"
  150. :confirm-text="$t('medicineToolsPage.calendarConfirm')"
  151. @close="calendarShow = false"
  152. @confirm="onCalendarConfirm"
  153. />
  154. </view>
  155. </template>
  156. <script>
  157. import USearch from 'uview-plus/components/u-search/u-search.vue'
  158. import UTabs from 'uview-plus/components/u-tabs/u-tabs.vue'
  159. import UButton from 'uview-plus/components/u-button/u-button.vue'
  160. import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
  161. import UCalendar from 'uview-plus/components/u-calendar/u-calendar.vue'
  162. import tabPage from '@/mixins/tabPage'
  163. import { queryDrug, calculateWithdrawal, checkCompatibility } from '@/api/medication'
  164. function todayYmd() {
  165. const d = new Date()
  166. const p = (n) => String(n).padStart(2, '0')
  167. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
  168. }
  169. /** 以「当前本地日期」为基准偏移整年,得到 YYYY-MM-DD */
  170. function addYearsFromToday(deltaYears) {
  171. const t = new Date()
  172. t.setFullYear(t.getFullYear() + deltaYears)
  173. const p = (n) => String(n).padStart(2, '0')
  174. return `${t.getFullYear()}-${p(t.getMonth() + 1)}-${p(t.getDate())}`
  175. }
  176. function normalizeCalendarValue(val) {
  177. if (val == null) return ''
  178. if (typeof val === 'string') return val.length >= 10 ? val.slice(0, 10) : val
  179. if (Array.isArray(val) && val.length) {
  180. return normalizeCalendarValue(val[0])
  181. }
  182. if (typeof val === 'object' && val.date) {
  183. const d = val.date instanceof Date ? val.date : new Date(val.date)
  184. if (!Number.isNaN(d.getTime())) {
  185. const p = (n) => String(n).padStart(2, '0')
  186. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
  187. }
  188. }
  189. return ''
  190. }
  191. /** 与 u-calendar 内 getMonths 一致:起止 YYYY-MM-DD 之间(含首尾月)的月份个数 */
  192. function countMonthsInclusive(minYmd, maxYmd) {
  193. const p = (s) => {
  194. const a = (s || '').split('-').map((x) => parseInt(x, 10))
  195. return { y: a[0], m: a[1] }
  196. }
  197. const A = p(minYmd)
  198. const B = p(maxYmd)
  199. if (!A.y || !A.m || !B.y || !B.m) return 1
  200. return (B.y - A.y) * 12 + (B.m - A.m) + 1
  201. }
  202. /** 用药配伍:最多药物行数 */
  203. const COMPAT_MAX_ROWS = 20
  204. export default {
  205. components: {
  206. 'up-search': USearch,
  207. 'up-tabs': UTabs,
  208. 'up-button': UButton,
  209. 'up-icon': UIcon,
  210. 'up-calendar': UCalendar
  211. },
  212. mixins: [tabPage],
  213. data() {
  214. return {
  215. navTitleKey: 'homeGrid.medicineTools',
  216. tabIndex: 0,
  217. queryLoading: false,
  218. calcLoading: false,
  219. compatLoading: false,
  220. qDrugKeyword: '',
  221. qDisplay: { type: '—', withdrawal: '—', guide: '—', taboo: '—' },
  222. wDrugKeyword: '',
  223. stopDateStr: '',
  224. calendarShow: false,
  225. calendarRenderKey: 0,
  226. wDisplay: { withdrawal: '—', endDate: '—' },
  227. compatMaxRows: COMPAT_MAX_ROWS,
  228. compatDrugUid: 2,
  229. compatDrugRows: [
  230. { id: 1, keyword: '' },
  231. { id: 2, keyword: '' }
  232. ],
  233. cDisplay: { forbidden: '—', result: '—' }
  234. }
  235. },
  236. computed: {
  237. tabsList() {
  238. return [
  239. { name: this.$t('medicineToolsPage.tabQuery'), id: 'q' },
  240. { name: this.$t('medicineToolsPage.tabWithdrawal'), id: 'w' },
  241. { name: this.$t('medicineToolsPage.tabCompat'), id: 'c' }
  242. ]
  243. },
  244. /** u-calendar 默认 monthNum=3 只会生成 3 个月;设为 min~max 之间的月数才能滚满范围 */
  245. calendarMonthNum() {
  246. const n = countMonthsInclusive(this.calendarMinDate, this.calendarMaxDate)
  247. return Math.min(240, Math.max(1, n))
  248. },
  249. calendarMinDate() {
  250. return addYearsFromToday(-2)
  251. },
  252. calendarMaxDate() {
  253. return addYearsFromToday(2)
  254. },
  255. /** 日历打开时默认高亮:已选且在范围内则用已选,否则为今天 */
  256. calendarDefaultDate() {
  257. const min = this.calendarMinDate
  258. const max = this.calendarMaxDate
  259. const s = (this.stopDateStr || '').trim()
  260. if (s && s >= min && s <= max) return s
  261. return todayYmd()
  262. }
  263. },
  264. methods: {
  265. openStopDateCalendar() {
  266. this.calendarRenderKey += 1
  267. this.calendarShow = true
  268. },
  269. onCalendarConfirm(val) {
  270. this.calendarShow = false
  271. const ymd = normalizeCalendarValue(val)
  272. if (ymd) this.stopDateStr = ymd
  273. },
  274. onQueryDrug() {
  275. const drugName = (this.qDrugKeyword || '').trim()
  276. if (!drugName) {
  277. uni.showToast({ title: this.$t('medicineToolsPage.toastEmptyDrug'), icon: 'none' })
  278. return
  279. }
  280. if (this.queryLoading) return
  281. this.queryLoading = true
  282. queryDrug({ drugName })
  283. .then((res) => {
  284. const d = res.data || {}
  285. this.qDisplay = {
  286. type: d.drugTypeName || '—',
  287. withdrawal:
  288. d.withdrawalDays != null
  289. ? this.$t('medicineToolsPage.daysTpl', { n: d.withdrawalDays })
  290. : '—',
  291. guide: d.usageGuide || '—',
  292. taboo: d.incompatibilityCompanions || '—'
  293. }
  294. })
  295. .catch(() => {
  296. this.qDisplay = { type: '—', withdrawal: '—', guide: '—', taboo: '—' }
  297. })
  298. .finally(() => {
  299. this.queryLoading = false
  300. })
  301. },
  302. onCalcWithdrawal() {
  303. const drugName = (this.wDrugKeyword || '').trim()
  304. if (!drugName) {
  305. uni.showToast({ title: this.$t('medicineToolsPage.toastEmptyDrug'), icon: 'none' })
  306. return
  307. }
  308. if (!this.stopDateStr) {
  309. uni.showToast({ title: this.$t('medicineToolsPage.toastPickDate'), icon: 'none' })
  310. return
  311. }
  312. if (this.calcLoading) return
  313. this.calcLoading = true
  314. calculateWithdrawal({ drugName, stopDate: this.stopDateStr })
  315. .then((res) => {
  316. const d = res.data || {}
  317. this.wDisplay = {
  318. withdrawal:
  319. d.withdrawalDays != null
  320. ? this.$t('medicineToolsPage.daysTpl', { n: d.withdrawalDays })
  321. : '—',
  322. endDate: d.endDate || '—'
  323. }
  324. })
  325. .catch(() => {
  326. this.wDisplay = { withdrawal: '—', endDate: '—' }
  327. })
  328. .finally(() => {
  329. this.calcLoading = false
  330. })
  331. },
  332. addCompatDrugRow() {
  333. if (this.compatDrugRows.length >= COMPAT_MAX_ROWS) {
  334. uni.showToast({ title: this.$t('medicineToolsPage.toastCompatMax', { n: COMPAT_MAX_ROWS }), icon: 'none' })
  335. return
  336. }
  337. this.compatDrugUid += 1
  338. this.compatDrugRows.push({ id: this.compatDrugUid, keyword: '' })
  339. },
  340. removeCompatDrugRow(id) {
  341. if (this.compatDrugRows.length <= 1) {
  342. uni.showToast({ title: this.$t('medicineToolsPage.toastCompatMinRows'), icon: 'none' })
  343. return
  344. }
  345. this.compatDrugRows = this.compatDrugRows.filter((r) => r.id !== id)
  346. },
  347. onCompat() {
  348. const rows = this.compatDrugRows
  349. const drugNames = []
  350. for (let i = 0; i < rows.length; i++) {
  351. const raw = (rows[i].keyword || '').trim()
  352. if (!raw) continue
  353. drugNames.push(raw)
  354. }
  355. if (drugNames.length < 2) {
  356. this.cDisplay = { forbidden: '—', result: '—' }
  357. uni.showToast({ title: this.$t('medicineToolsPage.toastCompatNeedTwoResolved'), icon: 'none' })
  358. return
  359. }
  360. if (new Set(drugNames).size !== drugNames.length) {
  361. this.cDisplay = { forbidden: '—', result: '—' }
  362. uni.showToast({ title: this.$t('medicineToolsPage.toastCompatDup'), icon: 'none' })
  363. return
  364. }
  365. if (this.compatLoading) return
  366. this.compatLoading = true
  367. const pairTasks = []
  368. for (let i = 0; i < drugNames.length; i++) {
  369. for (let j = i + 1; j < drugNames.length; j++) {
  370. pairTasks.push(
  371. checkCompatibility({ drugName1: drugNames[i], drugName2: drugNames[j] }).then((res) => ({
  372. a: drugNames[i],
  373. b: drugNames[j],
  374. data: res.data || {}
  375. }))
  376. )
  377. }
  378. }
  379. Promise.all(pairTasks)
  380. .then((results) => {
  381. const badPairs = results.filter((item) => item.data.hasIncompatibility)
  382. const forbidden = badPairs.length > 0
  383. const listStr = badPairs
  384. .map(({ a, b, data }) => {
  385. const result = (data.compatResult || '').trim()
  386. return result
  387. ? this.$t('medicineToolsPage.compatPairResultTpl', { a, b, result })
  388. : this.$t('medicineToolsPage.compatPairTpl', { a, b })
  389. })
  390. .join(';')
  391. this.cDisplay = {
  392. forbidden: forbidden
  393. ? this.$t('medicineToolsPage.forbiddenYes')
  394. : this.$t('medicineToolsPage.forbiddenNo'),
  395. result: forbidden
  396. ? this.$t('medicineToolsPage.compatMultiForbidden', { list: listStr })
  397. : this.$t('medicineToolsPage.compatMultiSafe')
  398. }
  399. })
  400. .catch(() => {
  401. this.cDisplay = { forbidden: '—', result: '—' }
  402. })
  403. .finally(() => {
  404. this.compatLoading = false
  405. })
  406. }
  407. }
  408. }
  409. </script>
  410. <style lang="scss" scoped>
  411. @import '@/styles/morandi.scss';
  412. @import '@/styles/tab-page.scss';
  413. .mt-page {
  414. display: flex;
  415. flex-direction: column;
  416. min-width: 0;
  417. min-height: 100%;
  418. box-sizing: border-box;
  419. background: #ffffff;
  420. }
  421. .mt-scroll {
  422. flex: 1;
  423. min-height: 0;
  424. min-width: 0;
  425. height: 0;
  426. box-sizing: border-box;
  427. background: #ffffff;
  428. }
  429. .mt-tabs-wrap {
  430. padding: 16rpx 24rpx 12rpx;
  431. box-sizing: border-box;
  432. background: #ffffff;
  433. border-bottom: 1rpx solid #f0f0f0;
  434. }
  435. .mt-tabs {
  436. width: 100%;
  437. min-width: 0;
  438. }
  439. .mt-panel {
  440. display: flex;
  441. flex-direction: column;
  442. gap: 20rpx;
  443. min-width: 0;
  444. padding: 20rpx 24rpx 32rpx;
  445. box-sizing: border-box;
  446. }
  447. .mt-row {
  448. display: flex;
  449. flex-direction: row;
  450. align-items: center;
  451. gap: 16rpx;
  452. min-width: 0;
  453. }
  454. .mt-label {
  455. flex-shrink: 0;
  456. width: 160rpx;
  457. color: $morandi-text-secondary;
  458. }
  459. .mt-search {
  460. flex: 1;
  461. min-width: 0;
  462. }
  463. .mt-row--compat {
  464. align-items: stretch;
  465. }
  466. .mt-compat-remove {
  467. flex-shrink: 0;
  468. display: flex;
  469. flex-direction: row;
  470. align-items: center;
  471. justify-content: center;
  472. gap: 6rpx;
  473. padding: 0 8rpx;
  474. min-height: 64rpx;
  475. box-sizing: border-box;
  476. }
  477. .mt-compat-remove__txt {
  478. font-size: 22rpx;
  479. color: #9ca3af;
  480. line-height: 1.2;
  481. white-space: nowrap;
  482. }
  483. .mt-compat-add {
  484. display: flex;
  485. flex-direction: row;
  486. justify-content: flex-start;
  487. padding-top: 4rpx;
  488. }
  489. .mt-date-field {
  490. flex: 1;
  491. min-width: 0;
  492. display: flex;
  493. flex-direction: row;
  494. align-items: center;
  495. justify-content: space-between;
  496. gap: 12rpx;
  497. box-sizing: border-box;
  498. padding: 10rpx 18rpx;
  499. min-height: 72rpx;
  500. border: 1rpx solid #e8e8e8;
  501. border-radius: 8rpx;
  502. background: #ffffff;
  503. }
  504. .mt-date-field__txt {
  505. flex: 1;
  506. min-width: 0;
  507. font-size: 28rpx;
  508. line-height: 1.45;
  509. color: #303133;
  510. word-break: break-all;
  511. }
  512. .mt-date-field__txt--placeholder {
  513. color: #c0c4cc;
  514. }
  515. .mt-row-btn {
  516. display: flex;
  517. flex-direction: row;
  518. justify-content: center;
  519. padding-top: 8rpx;
  520. }
  521. .mt-row-btn :deep(.u-button) {
  522. min-width: 280rpx;
  523. }
  524. .mt-gap-lg {
  525. height: 32rpx;
  526. }
  527. .mt-line {
  528. color: $morandi-text;
  529. line-height: 1.55;
  530. word-break: break-word;
  531. overflow-wrap: anywhere;
  532. }
  533. .mt-line--block {
  534. display: block;
  535. }
  536. .mt-footer-spacer {
  537. height: 48rpx;
  538. }
  539. .mt-page.lang-bo {
  540. .mt-label {
  541. width: 200rpx;
  542. font-size: 24rpx;
  543. line-height: 1.75;
  544. letter-spacing: 2rpx;
  545. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  546. }
  547. .mt-line {
  548. font-size: 26rpx;
  549. line-height: 1.75;
  550. letter-spacing: 2rpx;
  551. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  552. }
  553. .mt-date-field__txt {
  554. font-size: 26rpx;
  555. line-height: 1.75;
  556. letter-spacing: 2rpx;
  557. font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  558. }
  559. }
  560. </style>