| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602 |
- <template>
- <!-- 用药工具:顶区 up-tabs;下为三个 Tab 表单与结果区(文案 i18n),白底 -->
- <view :class="pageRootClass" class="tab-page mt-page" :style="pageStyle">
- <scroll-view scroll-y class="mt-scroll" enable-back-to-top :show-scrollbar="false">
- <view class="mt-tabs-wrap">
- <up-tabs
- :current="tabIndex"
- class="mt-tabs"
- :list="tabsList"
- key-name="name"
- :scrollable="false"
- line-color="#22C55E"
- :active-style="{ color: '#15803d', fontWeight: '600' }"
- :inactive-style="{ color: '#78716c' }"
- @update:current="tabIndex = $event"
- />
- </view>
- <!-- 查询药物 -->
- <view v-show="tabIndex === 0" class="mt-panel">
- <view class="mt-row">
- <text class="mt-label text-body">{{ $t('medicineToolsPage.labelDrugName') }}</text>
- <up-search
- v-model="qDrugKeyword"
- class="mt-search"
- shape="round"
- :placeholder="$t('medicineToolsPage.searchDrugPlaceholder')"
- :show-action="false"
- :clearabled="true"
- bg-color="#f5f5f5"
- border-color="#e8e8e8"
- />
- </view>
- <view class="mt-row-btn">
- <up-button
- type="primary"
- shape="circle"
- :text="$t('medicineToolsPage.btnQuery')"
- :loading="queryLoading"
- @click="onQueryDrug"
- />
- </view>
- <view class="mt-gap-lg" />
- <text class="mt-line text-body">{{ $t('medicineToolsPage.labelDrugType') }}:{{ qDisplay.type }}</text>
- <text class="mt-line text-body">{{ $t('medicineToolsPage.labelWithdrawal') }}:{{ qDisplay.withdrawal }}</text>
- <text class="mt-line text-body mt-line--block">{{ $t('medicineToolsPage.labelGuide') }}:{{ qDisplay.guide }}</text>
- <text class="mt-line text-body mt-line--block">{{ $t('medicineToolsPage.labelTaboo') }}:{{ qDisplay.taboo }}</text>
- </view>
- <!-- 计算休药期 -->
- <view v-show="tabIndex === 1" class="mt-panel">
- <view class="mt-row">
- <text class="mt-label text-body">{{ $t('medicineToolsPage.labelDrugName') }}</text>
- <up-search
- v-model="wDrugKeyword"
- class="mt-search"
- shape="round"
- :placeholder="$t('medicineToolsPage.searchDrugPlaceholder')"
- :show-action="false"
- :clearabled="true"
- bg-color="#f5f5f5"
- border-color="#e8e8e8"
- />
- </view>
- <view class="mt-row">
- <text class="mt-label text-body">{{ $t('medicineToolsPage.labelStopDate') }}</text>
- <!-- 只读 picker:不用原生 disabled input,否则小程序上点击无法冒泡到父级,日历弹不出 -->
- <view class="mt-date-field" role="button" @tap.stop="openStopDateCalendar">
- <text
- class="mt-date-field__txt text-body"
- :class="{ 'mt-date-field__txt--placeholder': !stopDateStr }"
- >{{ stopDateStr || $t('medicineToolsPage.pickStopDate') }}</text>
- <up-icon name="arrow-right" color="#78716c" :size="18" />
- </view>
- </view>
- <view class="mt-row-btn">
- <up-button
- type="primary"
- shape="circle"
- :text="$t('medicineToolsPage.btnCalc')"
- :loading="calcLoading"
- @click="onCalcWithdrawal"
- />
- </view>
- <view class="mt-gap-lg" />
- <text class="mt-line text-body">{{ $t('medicineToolsPage.labelWithdrawal') }}:{{ wDisplay.withdrawal }}</text>
- <text class="mt-line text-body mt-line--block">{{ $t('medicineToolsPage.labelEndDate') }}:{{ wDisplay.endDate }}</text>
- </view>
- <!-- 用药配伍:默认 2 行,可添加至最多 20 行;至少保留 1 行 -->
- <view v-show="tabIndex === 2" class="mt-panel">
- <view v-for="(row, idx) in compatDrugRows" :key="row.id" class="mt-row mt-row--compat">
- <text class="mt-label text-body">{{ $t('medicineToolsPage.labelDrugNth', { n: idx + 1 }) }}</text>
- <up-search
- v-model="row.keyword"
- class="mt-search"
- shape="round"
- :placeholder="$t('medicineToolsPage.searchDrugPlaceholder')"
- :show-action="false"
- :clearabled="true"
- bg-color="#f5f5f5"
- border-color="#e8e8e8"
- />
- <view
- v-if="compatDrugRows.length > 2"
- class="mt-compat-remove"
- role="button"
- @tap="removeCompatDrugRow(row.id)"
- >
- <up-icon name="trash" color="#9ca3af" :size="20" />
- <text class="mt-compat-remove__txt text-body">{{ $t('medicineToolsPage.btnRemoveCompatRow') }}</text>
- </view>
- </view>
- <view class="mt-compat-add">
- <up-button
- type="success"
- plain
- hairline
- size="small"
- :text="$t('medicineToolsPage.btnAddCompatDrug')"
- :disabled="compatDrugRows.length >= compatMaxRows"
- @click="addCompatDrugRow"
- />
- </view>
- <view class="mt-row-btn">
- <up-button
- type="primary"
- shape="circle"
- :text="$t('medicineToolsPage.btnCompat')"
- :loading="compatLoading"
- @click="onCompat"
- />
- </view>
- <view class="mt-gap-lg" />
- <text class="mt-line text-body">{{ $t('medicineToolsPage.labelForbidden') }}:{{ cDisplay.forbidden }}</text>
- <text class="mt-line text-body mt-line--block">{{ $t('medicineToolsPage.labelCompatResult') }}:{{ cDisplay.result }}</text>
- </view>
- <view class="mt-footer-spacer" />
- </scroll-view>
- <up-calendar
- :key="calendarRenderKey"
- :show="calendarShow"
- mode="single"
- :month-switch="true"
- :min-date="calendarMinDate"
- :max-date="calendarMaxDate"
- :default-date="calendarDefaultDate"
- :month-num="calendarMonthNum"
- color="#22C55E"
- :round="16"
- :close-on-click-overlay="true"
- :title="$t('medicineToolsPage.calendarTitle')"
- :confirm-text="$t('medicineToolsPage.calendarConfirm')"
- @close="calendarShow = false"
- @confirm="onCalendarConfirm"
- />
- </view>
- </template>
- <script>
- import USearch from 'uview-plus/components/u-search/u-search.vue'
- import UTabs from 'uview-plus/components/u-tabs/u-tabs.vue'
- import UButton from 'uview-plus/components/u-button/u-button.vue'
- import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
- import UCalendar from 'uview-plus/components/u-calendar/u-calendar.vue'
- import tabPage from '@/mixins/tabPage'
- import pageViewport from '@/mixins/pageViewport'
- import { queryDrug, calculateWithdrawal, checkCompatibility } from '@/api/medication'
- function todayYmd() {
- const d = new Date()
- const p = (n) => String(n).padStart(2, '0')
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
- }
- /** 以「当前本地日期」为基准偏移整年,得到 YYYY-MM-DD */
- function addYearsFromToday(deltaYears) {
- const t = new Date()
- t.setFullYear(t.getFullYear() + deltaYears)
- const p = (n) => String(n).padStart(2, '0')
- return `${t.getFullYear()}-${p(t.getMonth() + 1)}-${p(t.getDate())}`
- }
- function normalizeCalendarValue(val) {
- if (val == null) return ''
- if (typeof val === 'string') return val.length >= 10 ? val.slice(0, 10) : val
- if (Array.isArray(val) && val.length) {
- return normalizeCalendarValue(val[0])
- }
- if (typeof val === 'object' && val.date) {
- const d = val.date instanceof Date ? val.date : new Date(val.date)
- if (!Number.isNaN(d.getTime())) {
- const p = (n) => String(n).padStart(2, '0')
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
- }
- }
- return ''
- }
- /** 与 u-calendar 内 getMonths 一致:起止 YYYY-MM-DD 之间(含首尾月)的月份个数 */
- function countMonthsInclusive(minYmd, maxYmd) {
- const p = (s) => {
- const a = (s || '').split('-').map((x) => parseInt(x, 10))
- return { y: a[0], m: a[1] }
- }
- const A = p(minYmd)
- const B = p(maxYmd)
- if (!A.y || !A.m || !B.y || !B.m) return 1
- return (B.y - A.y) * 12 + (B.m - A.m) + 1
- }
- /** 用药配伍:最多药物行数 */
- const COMPAT_MAX_ROWS = 20
- export default {
- components: {
- 'up-search': USearch,
- 'up-tabs': UTabs,
- 'up-button': UButton,
- 'up-icon': UIcon,
- 'up-calendar': UCalendar
- },
- mixins: [tabPage, pageViewport],
- data() {
- return {
- navTitleKey: 'homeGrid.medicineTools',
- tabIndex: 0,
- queryLoading: false,
- calcLoading: false,
- compatLoading: false,
- qDrugKeyword: '',
- qDisplay: { type: '—', withdrawal: '—', guide: '—', taboo: '—' },
- wDrugKeyword: '',
- stopDateStr: '',
- calendarShow: false,
- calendarRenderKey: 0,
- wDisplay: { withdrawal: '—', endDate: '—' },
- compatMaxRows: COMPAT_MAX_ROWS,
- compatDrugUid: 2,
- compatDrugRows: [
- { id: 1, keyword: '' },
- { id: 2, keyword: '' }
- ],
- cDisplay: { forbidden: '—', result: '—' }
- }
- },
- computed: {
- tabsList() {
- return [
- { name: this.$t('medicineToolsPage.tabQuery'), id: 'q' },
- { name: this.$t('medicineToolsPage.tabWithdrawal'), id: 'w' },
- { name: this.$t('medicineToolsPage.tabCompat'), id: 'c' }
- ]
- },
- /** u-calendar 默认 monthNum=3 只会生成 3 个月;设为 min~max 之间的月数才能滚满范围 */
- calendarMonthNum() {
- const n = countMonthsInclusive(this.calendarMinDate, this.calendarMaxDate)
- return Math.min(240, Math.max(1, n))
- },
- calendarMinDate() {
- return addYearsFromToday(-2)
- },
- calendarMaxDate() {
- return addYearsFromToday(2)
- },
- /** 日历打开时默认高亮:已选且在范围内则用已选,否则为今天 */
- calendarDefaultDate() {
- const min = this.calendarMinDate
- const max = this.calendarMaxDate
- const s = (this.stopDateStr || '').trim()
- if (s && s >= min && s <= max) return s
- return todayYmd()
- }
- },
- methods: {
- openStopDateCalendar() {
- this.calendarRenderKey += 1
- this.calendarShow = true
- },
- onCalendarConfirm(val) {
- this.calendarShow = false
- const ymd = normalizeCalendarValue(val)
- if (ymd) this.stopDateStr = ymd
- },
- onQueryDrug() {
- const drugName = (this.qDrugKeyword || '').trim()
- if (!drugName) {
- uni.showToast({ title: this.$t('medicineToolsPage.toastEmptyDrug'), icon: 'none' })
- return
- }
- if (this.queryLoading) return
- this.queryLoading = true
- queryDrug({ drugName })
- .then((res) => {
- const d = res.data || {}
- this.qDisplay = {
- type: d.drugTypeName || '—',
- withdrawal:
- d.withdrawalDays != null
- ? this.$t('medicineToolsPage.daysTpl', { n: d.withdrawalDays })
- : '—',
- guide: d.usageGuide || '—',
- taboo: d.incompatibilityCompanions || '—'
- }
- })
- .catch(() => {
- this.qDisplay = { type: '—', withdrawal: '—', guide: '—', taboo: '—' }
- })
- .finally(() => {
- this.queryLoading = false
- })
- },
- onCalcWithdrawal() {
- const drugName = (this.wDrugKeyword || '').trim()
- if (!drugName) {
- uni.showToast({ title: this.$t('medicineToolsPage.toastEmptyDrug'), icon: 'none' })
- return
- }
- if (!this.stopDateStr) {
- uni.showToast({ title: this.$t('medicineToolsPage.toastPickDate'), icon: 'none' })
- return
- }
- if (this.calcLoading) return
- this.calcLoading = true
- calculateWithdrawal({ drugName, stopDate: this.stopDateStr })
- .then((res) => {
- const d = res.data || {}
- this.wDisplay = {
- withdrawal:
- d.withdrawalDays != null
- ? this.$t('medicineToolsPage.daysTpl', { n: d.withdrawalDays })
- : '—',
- endDate: d.endDate || '—'
- }
- })
- .catch(() => {
- this.wDisplay = { withdrawal: '—', endDate: '—' }
- })
- .finally(() => {
- this.calcLoading = false
- })
- },
- addCompatDrugRow() {
- if (this.compatDrugRows.length >= COMPAT_MAX_ROWS) {
- uni.showToast({ title: this.$t('medicineToolsPage.toastCompatMax', { n: COMPAT_MAX_ROWS }), icon: 'none' })
- return
- }
- this.compatDrugUid += 1
- this.compatDrugRows.push({ id: this.compatDrugUid, keyword: '' })
- },
- removeCompatDrugRow(id) {
- if (this.compatDrugRows.length <= 1) {
- uni.showToast({ title: this.$t('medicineToolsPage.toastCompatMinRows'), icon: 'none' })
- return
- }
- this.compatDrugRows = this.compatDrugRows.filter((r) => r.id !== id)
- },
- onCompat() {
- const rows = this.compatDrugRows
- const drugNames = []
- for (let i = 0; i < rows.length; i++) {
- const raw = (rows[i].keyword || '').trim()
- if (!raw) continue
- drugNames.push(raw)
- }
- if (drugNames.length < 2) {
- this.cDisplay = { forbidden: '—', result: '—' }
- uni.showToast({ title: this.$t('medicineToolsPage.toastCompatNeedTwoResolved'), icon: 'none' })
- return
- }
- if (new Set(drugNames).size !== drugNames.length) {
- this.cDisplay = { forbidden: '—', result: '—' }
- uni.showToast({ title: this.$t('medicineToolsPage.toastCompatDup'), icon: 'none' })
- return
- }
- if (this.compatLoading) return
- this.compatLoading = true
- const pairTasks = []
- for (let i = 0; i < drugNames.length; i++) {
- for (let j = i + 1; j < drugNames.length; j++) {
- pairTasks.push(
- checkCompatibility({ drugName1: drugNames[i], drugName2: drugNames[j] }).then((res) => ({
- a: drugNames[i],
- b: drugNames[j],
- data: res.data || {}
- }))
- )
- }
- }
- Promise.all(pairTasks)
- .then((results) => {
- const badPairs = results.filter((item) => item.data.hasIncompatibility)
- const forbidden = badPairs.length > 0
- const listStr = badPairs
- .map(({ a, b, data }) => {
- const result = (data.compatResult || '').trim()
- return result
- ? this.$t('medicineToolsPage.compatPairResultTpl', { a, b, result })
- : this.$t('medicineToolsPage.compatPairTpl', { a, b })
- })
- .join(';')
- this.cDisplay = {
- forbidden: forbidden
- ? this.$t('medicineToolsPage.forbiddenYes')
- : this.$t('medicineToolsPage.forbiddenNo'),
- result: forbidden
- ? this.$t('medicineToolsPage.compatMultiForbidden', { list: listStr })
- : this.$t('medicineToolsPage.compatMultiSafe')
- }
- })
- .catch(() => {
- this.cDisplay = { forbidden: '—', result: '—' }
- })
- .finally(() => {
- this.compatLoading = false
- })
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- @import '@/styles/morandi.scss';
- @import '@/styles/tab-page.scss';
- .mt-page {
- display: flex;
- flex-direction: column;
- min-width: 0;
- width: 100%;
- height: 100%;
- min-height: 100%;
- overflow: hidden;
- box-sizing: border-box;
- background: #ffffff;
- }
- .mt-scroll {
- flex: 1;
- min-height: 0;
- min-width: 0;
- height: 0;
- box-sizing: border-box;
- background: #ffffff;
- }
- .mt-tabs-wrap {
- padding: 16rpx 24rpx 12rpx;
- box-sizing: border-box;
- background: #ffffff;
- border-bottom: 1rpx solid #f0f0f0;
- }
- .mt-tabs {
- width: 100%;
- min-width: 0;
- }
- .mt-panel {
- display: flex;
- flex-direction: column;
- gap: 20rpx;
- min-width: 0;
- padding: 20rpx 24rpx 32rpx;
- box-sizing: border-box;
- }
- .mt-row {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 16rpx;
- min-width: 0;
- }
- .mt-label {
- flex-shrink: 0;
- width: 160rpx;
- color: $morandi-text-secondary;
- }
- .mt-search {
- flex: 1;
- min-width: 0;
- }
- .mt-row--compat {
- align-items: stretch;
- }
- .mt-compat-remove {
- flex-shrink: 0;
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- gap: 6rpx;
- padding: 0 8rpx;
- min-height: 64rpx;
- box-sizing: border-box;
- }
- .mt-compat-remove__txt {
- font-size: 22rpx;
- color: #9ca3af;
- line-height: 1.2;
- white-space: nowrap;
- }
- .mt-compat-add {
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- padding-top: 4rpx;
- }
- .mt-date-field {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: space-between;
- gap: 12rpx;
- box-sizing: border-box;
- padding: 10rpx 18rpx;
- min-height: 72rpx;
- border: 1rpx solid #e8e8e8;
- border-radius: 8rpx;
- background: #ffffff;
- }
- .mt-date-field__txt {
- flex: 1;
- min-width: 0;
- font-size: 28rpx;
- line-height: 1.45;
- color: #303133;
- word-break: break-all;
- }
- .mt-date-field__txt--placeholder {
- color: #c0c4cc;
- }
- .mt-row-btn {
- display: flex;
- flex-direction: row;
- justify-content: center;
- padding-top: 8rpx;
- }
- .mt-row-btn :deep(.u-button) {
- min-width: 280rpx;
- }
- .mt-gap-lg {
- height: 32rpx;
- }
- .mt-line {
- color: $morandi-text;
- line-height: 1.55;
- word-break: break-word;
- overflow-wrap: anywhere;
- }
- .mt-line--block {
- display: block;
- }
- .mt-footer-spacer {
- height: 48rpx;
- }
- .mt-page.lang-bo {
- .mt-label {
- width: 200rpx;
- font-size: 24rpx;
- line-height: 1.75;
- letter-spacing: 2rpx;
- font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
- }
- .mt-line {
- font-size: 26rpx;
- line-height: 1.75;
- letter-spacing: 2rpx;
- font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
- }
- .mt-date-field__txt {
- font-size: 26rpx;
- line-height: 1.75;
- letter-spacing: 2rpx;
- font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
- }
- }
- </style>
|