| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- <template>
- <view class="message-page">
- <!-- 自定义顶栏:头像 + 组织名 + 搜索 -->
- <view class="custom-header">
- <view class="header-left" @click="onAvatarClick">
- <UserAvatar
- :name="currentUser.name"
- :id="currentUser.id"
- :src="currentUser.avatar"
- :size="80"
- unit="rpx"
- />
- <view class="org-info">
- <text class="user-name">{{ currentUser.name || '未设置' }}</text>
- <text class="org-name">{{ currentUser.orgName || '' }}</text>
- </view>
- </view>
- <view class="header-right">
- <view class="icon-btn" @click="onSearch">
- <image class="icon-img" src="/static/icons/search.svg" mode="aspectFit" />
- </view>
- </view>
- </view>
- <!-- 消息列表 -->
- <scroll-view
- class="message-list"
- scroll-y
- refresher-enabled
- :refresher-triggered="refresherTriggered"
- @refresherrefresh="onRefresh"
- >
- <block v-if="messageList && messageList.length">
- <view
- class="message-item"
- v-for="item in messageList"
- :key="item.id"
- @click="openChat(item)"
- >
- <view class="item-left">
- <view class="avatar-wrap">
- <SystemAvatar
- v-if="item.is_system || (item.app_id != null && item.app_id !== '')"
- :name="item.app_name || item.title"
- :size="96"
- unit="rpx"
- />
- <UserAvatar
- v-else
- :name="item.title"
- :id="item.id"
- :src="item.avatar"
- :size="96"
- unit="rpx"
- />
- <view v-if="item.unread" class="badge">{{ item.unread > 99 ? '99+' : item.unread }}</view>
- </view>
- <view class="item-content">
- <view class="item-row">
- <text class="item-title">{{ item.app_name || item.title }}</text>
- <text class="item-time">{{ item.time }}</text>
- </view>
- <text class="item-desc">{{ item.lastMessage }}</text>
- </view>
- </view>
- </view>
- </block>
- <view v-else-if="!hasToken" class="empty-tip" @click="goLogin">暂无会话,点击去登录</view>
- <view v-else-if="contactsFetchFailed" class="empty-tip" @click="retryFetch">请下拉刷新</view>
- <view v-else class="empty-tip">暂无消息</view>
- </scroll-view>
- </view>
- </template>
- <script>
- import { computed, onMounted, ref } from 'vue'
- import UserAvatar from '../../components/UserAvatar.vue'
- import SystemAvatar from '../../components/SystemAvatar.vue'
- import { useContacts } from '../../composables/useContacts'
- import { useWebSocket } from '../../composables/useWebSocket'
- import { fetchUnreadCountAndUpdateTabBar } from '../../composables/useUnreadBadge'
- import { chatStore } from '../../store/chat'
- import { getToken } from '../../utils/api'
- import { setupAppNotifications } from '../../utils/notificationSetup'
- const USER_KEY = 'current_user'
- function formatTime(str) {
- if (!str) return ''
- const d = new Date(str)
- if (isNaN(d.getTime())) return str
- const now = new Date()
- const isToday = d.toDateString() === now.toDateString()
- if (isToday) return d.getHours() + ':' + String(d.getMinutes()).padStart(2, '0')
- const yesterday = new Date(now)
- yesterday.setDate(yesterday.getDate() - 1)
- if (d.toDateString() === yesterday.toDateString()) return '昨天'
- return (d.getMonth() + 1) + '月' + d.getDate() + '日'
- }
- export default {
- components: { UserAvatar, SystemAvatar },
- setup() {
- const { fetchContacts } = useContacts()
- const { connect } = useWebSocket()
- const currentUser = ref({ name: '', id: '', avatar: '', orgName: '' })
- const refresherTriggered = ref(false)
- function loadCurrentUser() {
- try {
- const raw = uni.getStorageSync(USER_KEY)
- if (raw && typeof raw === 'object') {
- currentUser.value = {
- name: raw.name || raw.nickname || '',
- id: String(raw.id ?? raw.user_id ?? ''),
- avatar: raw.avatar || raw.avatar_url || '',
- orgName: raw.org_name || raw.orgName || ''
- }
- }
- } catch (e) {}
- }
- const messageList = computed(() => {
- return (chatStore.contacts || []).map((c) => ({
- ...c,
- time: formatTime(c.time),
- unread: Number(c.unread_count ?? c.unread ?? 0) || 0
- }))
- })
- function openChat(item) {
- const id = item.user_id ?? item.id
- uni.navigateTo({ url: '/pages/chat/index?otherUserId=' + encodeURIComponent(id) })
- }
- async function retryFetch() {
- await fetchContacts()
- await fetchUnreadCountAndUpdateTabBar()
- }
- async function onRefresh() {
- refresherTriggered.value = true
- try {
- await retryFetch()
- } finally {
- refresherTriggered.value = false
- }
- }
- onMounted(() => {
- loadCurrentUser()
- fetchContacts()
- fetchUnreadCountAndUpdateTabBar()
- connect()
- // #ifdef APP-PLUS
- setupAppNotifications()
- // #endif
- })
- const hasToken = computed(() => !!getToken())
- const contactsFetchFailed = computed(() => chatStore.contactsFetchFailed)
- function goLogin() {
- uni.reLaunch({ url: '/pages/login/index' })
- }
- return {
- messageList,
- hasToken,
- contactsFetchFailed,
- retryFetch,
- fetchContacts,
- fetchUnreadCountAndUpdateTabBar,
- connect,
- openChat,
- goLogin,
- currentUser,
- loadCurrentUser,
- refresherTriggered,
- onRefresh
- }
- },
- async onShow() {
- if (this.loadCurrentUser) this.loadCurrentUser()
- if (this.connect) this.connect()
- if (this.fetchContacts) await this.fetchContacts()
- if (this.fetchUnreadCountAndUpdateTabBar) await this.fetchUnreadCountAndUpdateTabBar()
- },
- async onTabItemTap() {
- if (this.fetchContacts) await this.fetchContacts()
- if (this.fetchUnreadCountAndUpdateTabBar) await this.fetchUnreadCountAndUpdateTabBar()
- },
- methods: {
- onAvatarClick() {
- uni.navigateTo({ url: '/pages/profile/index' })
- },
- onSearch() {
- uni.navigateTo({ url: '/pages/search-center/index' })
- }
- }
- }
- </script>
- <style scoped>
- .message-page {
- /* 高度占满视口,不再手动减 tabBar,高度保持 100vh 以保证 scroll-view 有明确高度 */
- height: 100vh;
- display: flex;
- flex-direction: column;
- background: #fff;
- }
- /* 顶部安全区:88rpx 为无安全区时的最小间距(安卓等),max 保证刘海/状态栏下也足够 */
- .custom-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 24rpx 24rpx 24rpx 32rpx;
- padding-top: 88rpx;
- padding-top: max(88rpx, calc(24rpx + constant(safe-area-inset-top)));
- padding-top: max(88rpx, calc(24rpx + env(safe-area-inset-top)));
- background: #fff;
- border-bottom: 1rpx solid #eee;
- }
- .header-left {
- display: flex;
- align-items: center;
- flex: 1;
- min-width: 0;
- }
- .avatar-wrap :deep(.user-avatar) {
- flex-shrink: 0;
- }
- .org-info {
- margin-left: 24rpx;
- display: flex;
- flex-direction: column;
- min-width: 0;
- }
- .user-name {
- font-size: 32rpx;
- font-weight: 600;
- color: #333;
- }
- .org-name {
- font-size: 24rpx;
- color: #999;
- margin-top: 4rpx;
- }
- .header-right {
- display: flex;
- align-items: center;
- gap: 24rpx;
- }
- .icon-btn {
- width: 64rpx;
- height: 64rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .icon-img {
- width: 44rpx;
- height: 44rpx;
- opacity: 0.85;
- }
- .message-list {
- flex: 1;
- min-height: 0;
- height: 0;
- /* 使用框架提供的 tabBar 高度变量,为 H5 端等留出底部空间,避免与 tabBar 重叠 */
- padding-bottom: var(--window-bottom, 50px);
- box-sizing: border-box;
- }
- .empty-tip {
- padding: 80rpx 32rpx;
- text-align: center;
- font-size: 28rpx;
- color: #999;
- }
- .message-item {
- padding: 28rpx 32rpx;
- border-bottom: 1rpx solid #f0f0f0;
- }
- .item-left {
- display: flex;
- align-items: flex-start;
- }
- .avatar-wrap {
- position: relative;
- flex-shrink: 0;
- }
- .badge {
- position: absolute;
- top: -8rpx;
- right: -8rpx;
- min-width: 32rpx;
- height: 32rpx;
- line-height: 32rpx;
- padding: 0 8rpx;
- font-size: 20rpx;
- color: #fff;
- background: #f5222d;
- border-radius: 16rpx;
- text-align: center;
- }
- .item-content {
- flex: 1;
- margin-left: 24rpx;
- min-width: 0;
- }
- .item-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 8rpx;
- }
- .item-title {
- font-size: 30rpx;
- color: #333;
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .item-time {
- font-size: 24rpx;
- color: #999;
- flex-shrink: 0;
- margin-left: 16rpx;
- }
- .item-desc {
- font-size: 26rpx;
- color: #999;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- display: block;
- }
- </style>
|