| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549 |
- <template>
- <view :class="pageRootClass" class="tab-page msg-page">
- <view class="msg-head">
- <view class="msg-search">
- <up-icon class="msg-search__icon" name="search" color="#9A9590" :size="18" />
- <input
- v-model="query"
- class="msg-search__input"
- type="text"
- :placeholder="$t('messagePage.searchPlaceholder')"
- placeholder-class="msg-search__ph"
- confirm-type="search"
- />
- </view>
- </view>
- <view class="msg-scroll" :style="scrollWrapStyle">
- <view v-if="listLoading && !sessions.length" class="msg-empty">
- <text class="msg-empty__txt">{{ $t('messagePage.loading') }}</text>
- </view>
- <view v-else-if="!displayRows.length" class="msg-empty">
- <text class="msg-empty__txt">{{ emptyHint }}</text>
- </view>
- <up-virtual-list
- v-else
- class="msg-vlist"
- :list-data="displayRows"
- :item-height="slotHeightPx"
- :height="scrollAreaPx"
- :buffer="8"
- key-field="id"
- :scroll-top="vScrollTop"
- @update:scrollTop="vScrollTop = $event"
- @scroll="onVirtualListScroll"
- >
- <template #default="{ item }">
- <view class="msg-vlist__cell">
- <view
- class="msg-row"
- :class="{ 'msg-row--hl': highlightId === item.id }"
- :style="{ height: rowBodyPx + 'px' }"
- role="button"
- @tap="onRowTap(item)"
- @longpress.stop="onRowLongPress(item)"
- >
- <up-avatar
- v-if="avatarUrl(item)"
- class="msg-row__avatar"
- shape="circle"
- :src="avatarUrl(item)"
- size="44"
- font-size="16"
- />
- <up-avatar
- v-else
- class="msg-row__avatar"
- shape="circle"
- :text="avatarLetter(item)"
- size="44"
- font-size="16"
- :bg-color="avatarBg(item)"
- color="#fdfcfa"
- />
- <view class="msg-row__mid">
- <text class="text-body msg-row__title">{{ vetTitle(item) }}</text>
- <text class="text-body msg-row__preview">{{ previewText(item) }}</text>
- </view>
- <text class="msg-row__date">{{ formatDate(item.ts) }}</text>
- </view>
- </view>
- </template>
- </up-virtual-list>
- </view>
- </view>
- </template>
- <script>
- /**
- * 消息 Tab:对接 GET /app/consult/vet/session/list(M3)、POST .../hide(M4)
- */
- import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
- import UAvatar from 'uview-plus/components/u-avatar/u-avatar.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 {
- listAskerConsultSessions,
- hideAskerConsultSession,
- loadVetProfileCache
- } from '@/api/onlineConsult'
- import { loadBookingResourceCache } from '@/api/bookingService'
- const ROW_BODY_RPX = 168
- const ROW_GAP_RPX = 20
- const PAGE_SIZE = 24
- const CONSULT_DETAIL_PATH = '/package-a/consult-detail/index'
- const HIDDEN_SESSIONS_KEY = 'asker_hidden_consult_sessions'
- export default {
- components: {
- 'up-icon': UIcon,
- 'up-avatar': UAvatar,
- 'up-virtual-list': UVirtualList
- },
- mixins: [tabPage],
- data() {
- return {
- navTitleKey: 'nav.message',
- query: '',
- sessions: [],
- listLoading: false,
- listLoadingMore: false,
- listTotal: 0,
- pageNum: 1,
- pageSize: PAGE_SIZE,
- rowBodyPx: 76,
- marginPx: 10,
- slotHeightPx: 86,
- scrollAreaPx: 400,
- highlightId: '',
- headHeightPx: 0,
- vScrollTop: 0,
- loadMoreTimer: null,
- hiddenSessionIds: []
- }
- },
- computed: {
- queryTrim() {
- return (this.query || '').trim()
- },
- listNoMore() {
- return this.listTotal > 0 && this.sessions.length >= this.listTotal
- },
- visibleSessions() {
- const hidden = new Set(this.hiddenSessionIds)
- return this.sessions.filter((row) => !hidden.has(row.id))
- },
- filteredFull() {
- const q = this.queryTrim.toLowerCase()
- if (!q) return this.visibleSessions
- return this.visibleSessions.filter((row) => {
- const t = `${this.vetTitle(row)} ${this.previewText(row)}`.toLowerCase()
- return t.includes(q)
- })
- },
- displayRows() {
- return this.filteredFull
- },
- emptyHint() {
- if (this.queryTrim) {
- return this.$t('messagePage.empty')
- }
- return this.$t('messagePage.emptyList')
- },
- scrollWrapStyle() {
- const h = this.scrollAreaPx
- const top = this.headHeightPx
- return {
- height: `${h}px`,
- marginTop: `${top}px`
- }
- }
- },
- watch: {
- queryTrim() {
- this.vScrollTop = 0
- }
- },
- onShow() {
- this.loadHiddenSessionIds()
- if (ensureApiToken(false)) {
- this.loadSessionList(true)
- }
- },
- onReady() {
- try {
- this.marginPx = Math.ceil(uni.upx2px(ROW_GAP_RPX))
- this.rowBodyPx = Math.max(64, Math.ceil(uni.upx2px(ROW_BODY_RPX)))
- this.slotHeightPx = this.rowBodyPx + this.marginPx
- } catch (e) {
- this.marginPx = 10
- this.rowBodyPx = 76
- this.slotHeightPx = 86
- }
- this.headHeightPx = Math.ceil(uni.upx2px(132))
- this.applyScrollLayout()
- this.$nextTick(() => {
- uni.createSelectorQuery()
- .in(this)
- .select('.msg-head')
- .boundingClientRect((rect) => {
- if (rect && rect.height) {
- this.headHeightPx = Math.ceil(rect.height)
- this.applyScrollLayout()
- }
- })
- .exec()
- })
- },
- onUnload() {
- if (this.loadMoreTimer) clearTimeout(this.loadMoreTimer)
- },
- methods: {
- loadHiddenSessionIds() {
- try {
- const raw = uni.getStorageSync(HIDDEN_SESSIONS_KEY)
- if (!raw) {
- this.hiddenSessionIds = []
- return
- }
- const arr = typeof raw === 'string' ? JSON.parse(raw) : raw
- this.hiddenSessionIds = Array.isArray(arr) ? arr.map(String) : []
- } catch (e) {
- this.hiddenSessionIds = []
- }
- },
- saveHiddenSessionIds() {
- try {
- uni.setStorageSync(HIDDEN_SESSIONS_KEY, JSON.stringify(this.hiddenSessionIds))
- } catch (e) {
- /* noop */
- }
- },
- applyScrollLayout() {
- const sys = uni.getSystemInfoSync()
- const winH = sys.windowHeight || 600
- const head = this.headHeightPx || Math.ceil(uni.upx2px(132))
- this.scrollAreaPx = Math.max(200, winH - head - 30)
- },
- mapSessionRow(row) {
- const name = row.receiverName || ''
- const ts = row.lastMessageTime ? new Date(row.lastMessageTime).getTime() : 0
- return {
- id: String(row.id != null ? row.id : ''),
- sessionId: String(row.id != null ? row.id : ''),
- vetResourceId: row.receiverProviderId != null ? String(row.receiverProviderId) : '',
- receiverName: name,
- lastMessagePreview: row.lastMessagePreview || '',
- lastMessageTime: row.lastMessageTime || '',
- ts: Number.isFinite(ts) ? ts : 0,
- avatarText: name.slice(0, 1) || '?',
- raw: row
- }
- },
- loadSessionList(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
- }
- return listAskerConsultSessions({
- pageNum: this.pageNum,
- pageSize: this.pageSize
- })
- .then((res) => {
- const rows = (res.rows || []).map((row) => this.mapSessionRow(row))
- this.listTotal = res.total != null ? Number(res.total) : 0
- this.sessions = reset ? rows : this.sessions.concat(rows)
- this.$nextTick(() => this.tryAutoLoadMore())
- })
- .catch(() => {
- if (reset) {
- this.sessions = []
- 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.displayRows.length * this.slotHeightPx
- const viewH = this.scrollAreaPx
- if (totalH > 0 && totalH <= viewH) {
- this.loadSessionList(false)
- }
- },
- vetTitle(row) {
- if (row.receiverName) {
- return row.receiverName
- }
- return this.$t('messagePage.vetName', { n: '?' })
- },
- previewText(row) {
- const p = (row.lastMessagePreview || '').trim()
- return p || this.$t('messagePage.noPreview')
- },
- formatDate(ts) {
- if (!ts) return ''
- const d = new Date(ts)
- if (isNaN(d.getTime())) return ''
- const m = d.getMonth() + 1
- const day = d.getDate()
- if (this.lang === 'bo') {
- return `${m}/${day}`
- }
- return `${m}月${day}日`
- },
- avatarUrl(item) {
- if (!item || !item.vetResourceId) return ''
- const id = item.vetResourceId
- const fromProfile = loadVetProfileCache(id)
- const fromBooking = loadBookingResourceCache('004001', id)
- const url = (fromProfile && fromProfile.photoFileUrl) || (fromBooking && fromBooking.photoFileUrl)
- return url ? resolveResourceUrl(url) : ''
- },
- avatarBg(row) {
- const colors = ['#B8C5B8', '#C9B8A8', '#A8B4C9', '#C4B8C8', '#B5A896']
- const seed = row.vetResourceId || row.id || '0'
- let n = 0
- for (let i = 0; i < seed.length; i++) {
- n += seed.charCodeAt(i)
- }
- return colors[n % colors.length]
- },
- avatarLetter(row) {
- const t = row.avatarText || this.vetTitle(row)
- if (!t) return '?'
- return t.charAt(0)
- },
- onVirtualListScroll(scrollTop) {
- const stitle = typeof scrollTop === 'number' ? scrollTop : 0
- if (this.loadMoreTimer) clearTimeout(this.loadMoreTimer)
- this.loadMoreTimer = setTimeout(() => {
- this.loadMoreTimer = null
- this.tryLoadMoreOnScroll(st)
- }, 180)
- },
- tryLoadMoreOnScroll(scrollTop) {
- if (this.listLoading || this.listLoadingMore || this.listNoMore) return
- const totalH = this.displayRows.length * this.slotHeightPx
- const viewH = this.scrollAreaPx
- const threshold = Math.max(this.slotHeightPx * 2, 120)
- if (totalH <= viewH) {
- this.loadSessionList(false)
- return
- }
- if (scrollTop + viewH >= totalH - threshold) {
- this.loadSessionList(false)
- }
- },
- onRowTap(item) {
- if (!item || !item.sessionId) return
- const q = [
- `sessionId=${encodeURIComponent(item.sessionId)}`,
- `vetResourceId=${encodeURIComponent(item.vetResourceId || '')}`,
- `id=${encodeURIComponent(item.vetResourceId || '')}`,
- `name=${encodeURIComponent(item.receiverName || '')}`
- ].join('&')
- uni.navigateTo({
- url: `${CONSULT_DETAIL_PATH}?${q}`
- })
- },
- onRowLongPress(row) {
- this.highlightId = row.id
- uni.showActionSheet({
- itemList: [this.$t('messagePage.delete')],
- success: (res) => {
- if (res.tapIndex === 0) {
- this.removeConversation(row)
- }
- this.highlightId = ''
- },
- fail: () => {
- this.highlightId = ''
- }
- })
- },
- removeConversation(row) {
- if (!row || !row.sessionId) return
- hideAskerConsultSession(row.sessionId)
- .then(() => {
- const sid = String(row.sessionId)
- if (!this.hiddenSessionIds.includes(sid)) {
- this.hiddenSessionIds.push(sid)
- this.saveHiddenSessionIds()
- }
- const i = this.sessions.findIndex((x) => x.id === row.id)
- if (i !== -1) {
- this.sessions.splice(i, 1)
- }
- this.vScrollTop = 0
- })
- .catch(() => {
- uni.showToast({ title: this.$t('messagePage.deleteFail'), icon: 'none' })
- })
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- @import '@/styles/morandi.scss';
- @import '@/styles/tab-page.scss';
- .msg-page {
- display: flex;
- flex-direction: column;
- min-width: 0;
- min-height: 100%;
- box-sizing: border-box;
- background: $morandi-bg-page;
- }
- .msg-head {
- position: fixed;
- top: 80rpx;
- left: 0;
- right: 0;
- z-index: 100;
- flex-shrink: 0;
- padding: 24rpx 24rpx 20rpx;
- box-sizing: border-box;
- background: $morandi-bg-page;
- box-shadow: 0 4rpx 16rpx rgba(74, 69, 66, 0.06);
- }
- .msg-search {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 16rpx;
- min-width: 0;
- padding: 18rpx 22rpx;
- border-radius: 20rpx;
- background: $morandi-bg-card-inner;
- border: 1rpx solid $morandi-border-soft;
- }
- .msg-search__icon {
- flex-shrink: 0;
- }
- .msg-search__input {
- flex: 1;
- min-width: 0;
- font-size: 28rpx;
- line-height: 1.4;
- color: $morandi-text-secondary;
- }
- .msg-search__ph {
- color: $morandi-text-muted;
- font-size: 26rpx;
- }
- .msg-scroll {
- flex: 1;
- min-height: 0;
- min-width: 0;
- padding: 0 16rpx 16rpx;
- box-sizing: border-box;
- }
- .msg-vlist {
- width: 100%;
- }
- .msg-vlist__cell {
- height: 100%;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- }
- .msg-row {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 20rpx;
- min-width: 0;
- padding: 22rpx 20rpx;
- box-sizing: border-box;
- border-radius: 16rpx;
- background: $morandi-bg-card;
- border: 1rpx solid $morandi-border;
- flex-shrink: 0;
- }
- .msg-row--hl {
- background: $morandi-highlight;
- border-color: $morandi-highlight-border;
- }
- .msg-row__avatar {
- flex-shrink: 0;
- }
- .msg-row__mid {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 10rpx;
- min-height: 0;
- }
- .msg-row__title {
- font-size: 32rpx;
- font-weight: 600;
- line-height: 1.35;
- color: $morandi-text;
- word-break: break-word;
- overflow-wrap: anywhere;
- }
- .msg-row__preview {
- font-size: 24rpx;
- line-height: 1.45;
- color: $morandi-text-muted;
- word-break: break-word;
- overflow-wrap: anywhere;
- }
- .msg-row__date {
- flex-shrink: 0;
- align-self: flex-start;
- margin-top: 4rpx;
- font-size: 22rpx;
- color: $morandi-text-soft;
- }
- .msg-empty {
- padding: 80rpx 24rpx;
- display: flex;
- justify-content: center;
- }
- .msg-empty__txt {
- font-size: 26rpx;
- color: $morandi-text-muted;
- }
- </style>
|