| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576 |
- <template>
- <!-- 在线问诊:见 doc/app/在线问诊/在线问诊接口说明.md -->
- <view :class="pageRootClass" class="tab-page oc-page" :style="pageStyle">
- <view class="oc-top">
- <view class="oc-card">
- <up-search
- v-model="searchKeyword"
- shape="round"
- :placeholder="$t('onlineClinicPage.searchPlaceholder')"
- :show-action="false"
- :clearabled="true"
- bg-color="#f5f5f5"
- border-color="#e8e8e8"
- />
- </view>
- </view>
- <view class="oc-body">
- <view v-if="listLoading && !vetRows.length" class="oc-empty">
- <text class="text-body oc-empty__txt">{{ $t('onlineClinicPage.loading') }}</text>
- </view>
- <view v-else-if="!vetRows.length" class="oc-empty">
- <text class="text-body oc-empty__txt">{{ $t('onlineClinicPage.empty') }}</text>
- </view>
- <view v-else class="oc-list-wrap">
- <up-virtual-list
- ref="ocVList"
- class="oc-vlist"
- :list-data="vetRows"
- :item-height="slotHeightPx"
- :height="listHeightPx"
- :buffer="6"
- key-field="id"
- :scroll-top="vScrollTop"
- @update:scrollTop="vScrollTop = $event"
- @scroll="onVirtualListScroll"
- >
- <template #default="{ item }">
- <view class="oc-cell">
- <view class="oc-item" :style="{ height: rowBodyPx + 'px' }">
- <view class="oc-item__row1">
- <up-avatar
- shape="circle"
- :src="avatarUrl(item)"
- :text="avatarUrl(item) ? '' : item.avatarText"
- size="48"
- font-size="18"
- bg-color="#9ca3af"
- color="#ffffff"
- />
- <view class="oc-item__right">
- <view class="oc-item__head">
- <view class="oc-item__titles">
- <text class="oc-item__name text-body">{{ nameFor(item) }}</text>
- <text v-if="subFor(item)" class="oc-item__sub text-body">{{ subFor(item) }}</text>
- </view>
- <up-button
- class="oc-item__btn"
- type="success"
- shape="circle"
- plain
- hairline
- size="small"
- :text="$t('onlineClinicPage.btnProfile')"
- :custom-style="profileBtnStyle"
- @click="onProfile(item)"
- />
- </view>
- <text class="oc-item__intro text-body">
- <text class="oc-item__intro-k">{{ $t('onlineClinicPage.introLabel') }}</text>
- {{ introFor(item) }}
- </text>
- </view>
- </view>
- <up-button
- type="success"
- shape="circle"
- :text="$t('onlineClinicPage.btnConsult')"
- :custom-style="{ width: '100%' }"
- :loading="consultingId === item.id"
- @click="onConsult(item)"
- />
- </view>
- </view>
- </template>
- </up-virtual-list>
- </view>
- </view>
- </view>
- </template>
- <script>
- import USearch from 'uview-plus/components/u-search/u-search.vue'
- import UAvatar from 'uview-plus/components/u-avatar/u-avatar.vue'
- import UButton from 'uview-plus/components/u-button/u-button.vue'
- import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
- import tabPage from '@/mixins/tabPage'
- import { resolveResourceUrl } from '@/utils/resourceUrl'
- import { ensureApiToken } from '@/utils/apiAuth'
- import {
- listOnlineConsultVets,
- openOnlineConsultSession,
- saveVetProfileCache
- } from '@/api/onlineConsult'
- const ROW_BODY_RPX = 320
- const ROW_GAP_RPX = 20
- const OC_BODY_PAD_RPX = 32
- const LIST_PAGE_SIZE = 20
- const CONSULT_DETAIL_PATH = '/package-a/consult-detail/index'
- const VET_PROFILE_PATH = '/package-a/vet-profile/index'
- export default {
- components: {
- 'up-search': USearch,
- 'up-avatar': UAvatar,
- 'up-button': UButton,
- 'up-virtual-list': UVirtualList
- },
- mixins: [tabPage],
- data() {
- return {
- navTitleKey: 'homeGrid.onlineClinic',
- searchKeyword: '',
- vetRows: [],
- listLoading: false,
- listLoadingMore: false,
- listTotal: 0,
- pageNum: 1,
- pageSize: LIST_PAGE_SIZE,
- consultingId: '',
- _searchTimer: null,
- loadMoreTimer: null,
- rowBodyPx: 150,
- marginPx: 5,
- slotHeightPx: 155,
- listHeightPx: 400,
- pageHeightPx: 0,
- vScrollTop: 0,
- profileBtnStyle: { flexShrink: 0, marginLeft: '8px' }
- }
- },
- computed: {
- listNoMore() {
- return this.listTotal > 0 && this.vetRows.length >= this.listTotal
- },
- pageStyle() {
- if (this.pageHeightPx > 0) {
- return { height: `${this.pageHeightPx}px` }
- }
- return {}
- }
- },
- watch: {
- searchKeyword() {
- this.vScrollTop = 0
- clearTimeout(this._searchTimer)
- this._searchTimer = setTimeout(() => {
- this.loadVetList(true)
- }, 300)
- }
- },
- onShow() {
- if (ensureApiToken(false)) {
- this.loadVetList(true)
- }
- this.$nextTick(() => this.calcLayoutHeights())
- },
- onReady() {
- try {
- this.marginPx = Math.ceil(uni.upx2px(ROW_GAP_RPX))
- this.rowBodyPx = Math.max(120, Math.ceil(uni.upx2px(ROW_BODY_RPX)))
- this.slotHeightPx = this.rowBodyPx + this.marginPx
- } catch (e) {
- this.marginPx = 5
- this.rowBodyPx = 150
- this.slotHeightPx = 155
- }
- this.applyListHeightFallback()
- this.$nextTick(() => this.calcLayoutHeights())
- },
- onUnload() {
- if (this._searchTimer) clearTimeout(this._searchTimer)
- if (this.loadMoreTimer) clearTimeout(this.loadMoreTimer)
- },
- methods: {
- mapVetRow(row) {
- const name = row.resourceName || ''
- return {
- id: String(row.id != null ? row.id : ''),
- resourceName: name,
- introduction: row.introduction || '',
- contactPhone: row.contactPhone || '',
- affiliatedUnit: row.affiliatedUnit || '',
- detailAddress: row.detailAddress || '',
- serviceStartTime: row.serviceStartTime || '',
- serviceEndTime: row.serviceEndTime || '',
- photoFileUrl: row.photoFileUrl || '',
- avatarText: name.slice(0, 1) || '兽',
- raw: row
- }
- },
- loadVetList(reset = false) {
- if (!ensureApiToken()) return Promise.resolve()
- if (!reset) {
- if (this.listLoading || this.listLoadingMore || this.listNoMore) {
- return Promise.resolve()
- }
- this.pageNum += 1
- this.listLoadingMore = true
- } else {
- this.pageNum = 1
- this.listLoading = true
- }
- const params = {
- pageNum: this.pageNum,
- pageSize: this.pageSize
- }
- const kw = (this.searchKeyword || '').trim()
- if (kw) {
- params.resourceName = kw
- }
- return listOnlineConsultVets(params)
- .then((res) => {
- const rows = (res.rows || []).map((row) => this.mapVetRow(row))
- this.listTotal = res.total != null ? Number(res.total) : 0
- this.vetRows = reset ? rows : this.vetRows.concat(rows)
- this.$nextTick(() => {
- this.calcLayoutHeights()
- })
- })
- .catch(() => {
- if (reset) {
- this.vetRows = []
- this.listTotal = 0
- } else {
- this.pageNum -= 1
- }
- })
- .finally(() => {
- if (reset) {
- this.listLoading = false
- } else {
- this.listLoadingMore = false
- }
- })
- },
- tryAutoLoadMore() {
- if (this.listLoading || this.listLoadingMore || this.listNoMore) return
- const totalH = this.vetRows.length * this.slotHeightPx
- const viewH = this.listHeightPx
- if (totalH > 0 && totalH <= viewH) {
- this.loadVetList(false)
- }
- },
- onVirtualListScroll(scrollTop) {
- if (this.loadMoreTimer) clearTimeout(this.loadMoreTimer)
- this.loadMoreTimer = setTimeout(() => {
- this.loadMoreTimer = null
- this.tryLoadMoreOnScroll(typeof scrollTop === 'number' ? scrollTop : 0)
- }, 180)
- },
- tryLoadMoreOnScroll(scrollTop) {
- if (this.listLoading || this.listLoadingMore || this.listNoMore) return
- const totalH = this.vetRows.length * this.slotHeightPx
- const viewH = this.listHeightPx
- const threshold = Math.max(this.slotHeightPx * 2, 120)
- if (totalH <= viewH) {
- this.loadVetList(false)
- return
- }
- if (scrollTop + viewH >= totalH - threshold) {
- this.loadVetList(false)
- }
- },
- getNavigationBarHeight() {
- const sys = uni.getSystemInfoSync()
- const statusBar = sys.statusBarHeight || 0
- // #ifdef MP-WEIXIN
- try {
- const menu = uni.getMenuButtonBoundingClientRect()
- if (menu && menu.height) {
- return statusBar + (menu.top - statusBar) * 2 + menu.height
- }
- } catch (e) {
- /* noop */
- }
- // #endif
- return statusBar + 44
- },
- getViewportContentHeight() {
- const sys = uni.getSystemInfoSync()
- const navH = this.getNavigationBarHeight()
- // #ifdef H5
- if (typeof window !== 'undefined' && window.innerHeight) {
- return Math.max(320, window.innerHeight - navH)
- }
- // #endif
- const screenH = sys.screenHeight || sys.windowHeight || 600
- return Math.max(320, screenH - navH)
- },
- applyListHeightFallback() {
- const contentH = this.getViewportContentHeight()
- const topBlock = Math.ceil(uni.upx2px(100))
- const bodyPad = Math.ceil(uni.upx2px(OC_BODY_PAD_RPX))
- this.pageHeightPx = contentH
- this.listHeightPx = Math.max(240, contentH - topBlock - bodyPad)
- },
- calcLayoutHeights() {
- const contentH = this.getViewportContentHeight()
- this.pageHeightPx = contentH
- uni.createSelectorQuery()
- .in(this)
- .select('.oc-body')
- .boundingClientRect((bodyRect) => {
- if (bodyRect && bodyRect.height > 0) {
- this.listHeightPx = Math.max(240, Math.floor(bodyRect.height))
- } else {
- const topFallback = Math.ceil(uni.upx2px(100))
- const bodyPad = Math.ceil(uni.upx2px(OC_BODY_PAD_RPX))
- this.listHeightPx = Math.max(240, contentH - topFallback - bodyPad)
- }
- this.$nextTick(() => {
- this.$refs.ocVList?.measureContainerHeight?.()
- this.tryAutoLoadMore()
- })
- })
- .exec()
- },
- avatarUrl(item) {
- if (item && item.photoFileUrl) {
- return resolveResourceUrl(item.photoFileUrl)
- }
- return ''
- },
- nameFor(item) {
- return item.resourceName || this.$t('onlineClinicPage.nameFallback')
- },
- subFor(item) {
- if (item.affiliatedUnit) {
- return item.affiliatedUnit
- }
- if (item.serviceStartTime && item.serviceEndTime) {
- return this.$t('onlineClinicPage.serviceHoursTpl', {
- start: item.serviceStartTime,
- end: item.serviceEndTime
- })
- }
- return ''
- },
- introFor(item) {
- return item.introduction || this.$t('onlineClinicPage.noIntro')
- },
- onProfile(item) {
- if (!item || !item.id) return
- saveVetProfileCache(item.id, item.raw || item)
- uni.navigateTo({
- url: `${VET_PROFILE_PATH}?id=${encodeURIComponent(item.id)}`
- })
- },
- onConsult(item) {
- if (!item || !item.id || this.consultingId) return
- if (!ensureApiToken()) return
- saveVetProfileCache(item.id, item.raw || item)
- this.consultingId = item.id
- uni.showLoading({ title: this.$t('onlineClinicPage.openingSession'), mask: true })
- openOnlineConsultSession({ vetResourceId: Number(item.id) || item.id })
- .then((res) => {
- const session = res.data || {}
- const sessionId = session.id != null ? String(session.id) : ''
- if (!sessionId) {
- return
- }
- const receiverName = session.receiverName || item.resourceName || ''
- const q = [
- `sessionId=${encodeURIComponent(sessionId)}`,
- `vetResourceId=${encodeURIComponent(item.id)}`,
- `id=${encodeURIComponent(item.id)}`,
- `name=${encodeURIComponent(receiverName)}`,
- `sub=${encodeURIComponent(this.subFor(item))}`,
- `intro=${encodeURIComponent(item.introduction || '')}`,
- `avatar=${encodeURIComponent(item.avatarText || '')}`
- ].join('&')
- uni.navigateTo({ url: `${CONSULT_DETAIL_PATH}?${q}` })
- })
- .finally(() => {
- this.consultingId = ''
- uni.hideLoading()
- })
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- @import '@/styles/morandi.scss';
- @import '@/styles/tab-page.scss';
- .oc-page {
- display: flex;
- flex-direction: column;
- min-width: 0;
- width: 100%;
- height: 100%;
- min-height: 100%;
- overflow: hidden;
- box-sizing: border-box;
- background: $morandi-bg-page;
- }
- .oc-top {
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- min-width: 0;
- }
- .oc-card {
- background: #ffffff;
- padding: 16rpx 20rpx;
- box-sizing: border-box;
- border-bottom: 1rpx solid $morandi-border-soft;
- }
- .oc-body {
- flex: 1;
- min-height: 0;
- height: 0;
- width: 100%;
- min-width: 0;
- display: flex;
- flex-direction: column;
- padding: 0 24rpx 16rpx;
- box-sizing: border-box;
- overflow: hidden;
- background: $morandi-bg-page;
- }
- .oc-list-wrap {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- min-width: 0;
- }
- .oc-empty {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 0;
- padding: 48rpx 24rpx;
- box-sizing: border-box;
- }
- .oc-empty__txt {
- color: $morandi-text-muted;
- }
- .oc-vlist {
- flex: 1;
- min-height: 0;
- width: 100%;
- height: 100%;
- box-sizing: border-box;
- padding: 10rpx 0 0;
- }
- .oc-cell {
- height: 100%;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- padding-bottom: 10rpx;
- }
- .oc-item {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- gap: 16rpx;
- padding: 20rpx;
- box-sizing: border-box;
- background: #ffffff;
- border-radius: 12rpx;
- border: 1rpx solid $morandi-border-soft;
- min-width: 0;
- flex-shrink: 0;
- overflow: hidden;
- }
- .oc-item__row1 {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 16rpx;
- min-width: 0;
- }
- .oc-item__right {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 10rpx;
- }
- .oc-item__head {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 12rpx;
- min-width: 0;
- }
- .oc-item__titles {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 4rpx;
- }
- .oc-item__btn {
- width: 86rpx;
- }
- .oc-item__name {
- font-size: 30rpx;
- font-weight: 600;
- color: #111827;
- line-height: 1.35;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .oc-item__sub {
- font-size: 24rpx;
- color: $morandi-text-secondary;
- line-height: 1.35;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .oc-item__intro {
- font-size: 22rpx;
- color: $morandi-text-muted;
- line-height: 1.45;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
- overflow: hidden;
- word-break: break-word;
- }
- .oc-item__intro-k {
- color: $morandi-text-muted;
- font-weight: 500;
- }
- .oc-page.lang-bo {
- .oc-item__name {
- font-size: 26rpx;
- }
- .oc-item__sub {
- font-size: 22rpx;
- }
- }
- </style>
|