363 lines
8.6 KiB
Plaintext
363 lines
8.6 KiB
Plaintext
<template>
|
|
<view class="page">
|
|
<!-- 顶部导航栏 -->
|
|
<view class="nav-bar">
|
|
<view class="nav-tabs">
|
|
<view class="nav-tab" :class="{ active: activeTab == 0 }" @click="switchTab(0)">
|
|
<text class="nav-tab-txt" :class="{ active: activeTab == 0 }">消息</text>
|
|
</view>
|
|
<view class="nav-tab" :class="{ active: activeTab == 1 }" @click="switchTab(1)">
|
|
<text class="nav-tab-txt" :class="{ active: activeTab == 1 }">缘友</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 消息 Tab:会话列表 -->
|
|
<scroll-view v-if="activeTab == 0" class="list-scroll" direction="vertical"
|
|
:refresher-enabled="true" :refresher-triggered="refreshing"
|
|
@refresherrefresh="loadConversations">
|
|
<view v-if="conversations.length == 0" class="empty">
|
|
<text class="empty-icon">💬</text>
|
|
<text class="empty-text">还没有聊天,去「缘友」找人聊聊吧</text>
|
|
</view>
|
|
<view v-for="(item, idx) in conversations" :key="idx" class="conv-item"
|
|
@click="openChat(item.peer_id)">
|
|
<view class="avatar-wrap">
|
|
<image class="avatar" :src="item.avatar || defaultAvatar" mode="aspectFill"></image>
|
|
<view v-if="item.online" class="online-dot"></view>
|
|
</view>
|
|
<view class="conv-main">
|
|
<view class="conv-row">
|
|
<text class="conv-name">{{ item.nickname || '用户' + item.peer_id }}</text>
|
|
<text class="conv-time">{{ fmtTime(item.last_time) }}</text>
|
|
</view>
|
|
<view class="conv-row">
|
|
<text class="conv-last">{{ item.is_self ? '我: ' : '' }}{{ previewContent(item) }}</text>
|
|
<view v-if="item.unread_count > 0" class="badge">
|
|
<text class="badge-txt">{{ item.unread_count > 99 ? '99+' : item.unread_count }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
|
|
<!-- 缘友 Tab:在线用户,点击直接发起聊天 -->
|
|
<scroll-view v-else class="list-scroll" direction="vertical"
|
|
:refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="loadOnline">
|
|
<view v-if="onlineUsers.length == 0" class="empty">
|
|
<text class="empty-icon">🌟</text>
|
|
<text class="empty-text">暂无在线缘友,稍后再来看看</text>
|
|
</view>
|
|
<view v-for="(u, idx) in onlineUsers" :key="idx" class="conv-item" @click="openChat(u.uid)">
|
|
<view class="avatar-wrap">
|
|
<image class="avatar" :src="u.avatar || defaultAvatar" mode="aspectFill"></image>
|
|
<view v-if="u.online" class="online-dot"></view>
|
|
</view>
|
|
<view class="conv-main">
|
|
<view class="conv-row">
|
|
<text class="conv-name">{{ u.nickname || '用户' + u.uid }}</text>
|
|
<text class="conv-time online-tag">在线</text>
|
|
</view>
|
|
<view class="conv-row">
|
|
<text class="conv-last">点击发起聊天</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
import { ref, onMounted, onShow } from 'vue'
|
|
import Api from '@/common/api-service.uts'
|
|
|
|
type Convo = {
|
|
peer_id : number
|
|
nickname : string
|
|
avatar : string
|
|
online : boolean
|
|
last_content : string
|
|
last_content_type : number
|
|
last_time : number
|
|
unread_count : number
|
|
is_self : boolean
|
|
}
|
|
|
|
type OnlineUser = {
|
|
uid : number
|
|
nickname : string
|
|
avatar : string
|
|
online : boolean
|
|
}
|
|
|
|
const defaultAvatar = 'https://randomuser.me/api/portraits/lego/1.jpg'
|
|
const activeTab = ref<number>(0)
|
|
const refreshing = ref<boolean>(false)
|
|
const conversations = ref<Convo[]>([])
|
|
const onlineUsers = ref<OnlineUser[]>([])
|
|
|
|
const switchTab = (i : number) => {
|
|
activeTab.value = i
|
|
if (i == 0) {
|
|
loadConversations()
|
|
} else {
|
|
loadOnline()
|
|
}
|
|
}
|
|
|
|
const loadConversations = () => {
|
|
refreshing.value = true
|
|
Api.message.conversations()
|
|
.then((res : UTSJSONObject) => {
|
|
const list = res.get('list') as UTSJSONObject[] | null
|
|
const arr : Convo[] = []
|
|
if (list != null) {
|
|
for (const it of list) {
|
|
arr.push({
|
|
peer_id : Number(it.get('peer_id')),
|
|
nickname : String(it.get('nickname') ?? ''),
|
|
avatar : String(it.get('avatar') ?? ''),
|
|
online : Boolean(it.get('online') ?? false),
|
|
last_content : String(it.get('last_content') ?? ''),
|
|
last_content_type : Number(it.get('last_content_type') ?? 0),
|
|
last_time : Number(it.get('last_time') ?? 0),
|
|
unread_count : Number(it.get('unread_count') ?? 0),
|
|
is_self : Boolean(it.get('is_self') ?? false)
|
|
} as Convo)
|
|
}
|
|
}
|
|
conversations.value = arr
|
|
})
|
|
.catch(() => {
|
|
conversations.value = []
|
|
})
|
|
.finally(() => {
|
|
refreshing.value = false
|
|
})
|
|
}
|
|
|
|
const loadOnline = () => {
|
|
refreshing.value = true
|
|
Api.user.online()
|
|
.then((res : UTSJSONObject) => {
|
|
let list : UTSJSONObject[] | null = null
|
|
if (res.containsKey('list')) {
|
|
list = res.get('list') as UTSJSONObject[] | null
|
|
} else {
|
|
list = res as any as UTSJSONObject[]
|
|
}
|
|
const arr : OnlineUser[] = []
|
|
if (list != null) {
|
|
for (const it of list) {
|
|
const uid = Number(it.get('uid') ?? it.get('id') ?? 0)
|
|
if (uid <= 0) continue
|
|
const online = Boolean(it.get('online') ?? it.get('is_online') ?? (Number(it.get('online_status') ?? 0) == 1))
|
|
arr.push({
|
|
uid : uid,
|
|
nickname : String(it.get('nickname') ?? ''),
|
|
avatar : String(it.get('avatar') ?? ''),
|
|
online : online
|
|
} as OnlineUser)
|
|
}
|
|
}
|
|
onlineUsers.value = arr
|
|
})
|
|
.catch(() => {
|
|
onlineUsers.value = []
|
|
})
|
|
.finally(() => {
|
|
refreshing.value = false
|
|
})
|
|
}
|
|
|
|
const previewContent = (item : Convo) : string => {
|
|
if (item.last_content_type == 1) return '[图片]'
|
|
if (item.last_content_type == 2) return '[语音]'
|
|
return item.last_content || '[暂无消息]'
|
|
}
|
|
|
|
const fmtTime = (sec : number) : string => {
|
|
if (! sec) return ''
|
|
const d = new Date(sec * 1000)
|
|
const now = new Date()
|
|
const diff = (now.getTime() - d.getTime()) / 1000
|
|
if (diff < 60) return '刚刚'
|
|
if (diff < 3600) return Math.floor(diff / 60) + '分钟前'
|
|
if (diff < 86400) return Math.floor(diff / 3600) + '小时前'
|
|
if (diff < 86400 * 2) return '昨天'
|
|
const y = d.getFullYear()
|
|
const m = d.getMonth() + 1
|
|
const day = d.getDate()
|
|
if (y == now.getFullYear()) return `${m}月${day}日`
|
|
return `${y}年${m}月${day}日`
|
|
}
|
|
|
|
const openChat = (uid : number) => {
|
|
uni.navigateTo({ url: `/pages/chat/chat?uid=${uid}` })
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadConversations()
|
|
})
|
|
|
|
// 从聊天页返回时刷新会话(更新未读/最新消息)
|
|
onShow(() => {
|
|
if (activeTab.value == 0) {
|
|
loadConversations()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style>
|
|
.page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
background-color: #f5f6f8;
|
|
}
|
|
|
|
.nav-bar {
|
|
padding-top: var(--status-bar-height);
|
|
height: 90rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background-color: #fff;
|
|
border-bottom: 1rpx solid #eee;
|
|
}
|
|
|
|
.nav-tabs {
|
|
display: flex;
|
|
flex-direction: row;
|
|
}
|
|
|
|
.nav-tab {
|
|
margin: 0 24rpx;
|
|
padding: 10rpx 0;
|
|
}
|
|
|
|
.nav-tab-txt {
|
|
font-size: 32rpx;
|
|
color: #999;
|
|
}
|
|
|
|
.nav-tab-txt.active {
|
|
font-size: 38rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
.list-scroll {
|
|
flex: 1;
|
|
padding: 10rpx 20rpx;
|
|
}
|
|
|
|
.conv-item {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 24rpx 20rpx;
|
|
background: #fff;
|
|
border-radius: 20rpx;
|
|
margin-bottom: 16rpx;
|
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.avatar-wrap {
|
|
position: relative;
|
|
margin-right: 20rpx;
|
|
}
|
|
|
|
.avatar {
|
|
width: 88rpx;
|
|
height: 88rpx;
|
|
border-radius: 50%;
|
|
background-color: #eee;
|
|
}
|
|
|
|
.online-dot {
|
|
position: absolute;
|
|
right: 2rpx;
|
|
bottom: 2rpx;
|
|
width: 18rpx;
|
|
height: 18rpx;
|
|
border-radius: 50%;
|
|
background-color: #4CD964;
|
|
border: 3rpx solid #fff;
|
|
}
|
|
|
|
.conv-main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.conv-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.conv-name {
|
|
font-size: 32rpx;
|
|
color: #333;
|
|
font-weight: 500;
|
|
max-width: 360rpx;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.conv-time {
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
}
|
|
|
|
.online-tag {
|
|
color: #4CD964;
|
|
}
|
|
|
|
.conv-last {
|
|
font-size: 26rpx;
|
|
color: #999;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
margin-right: 16rpx;
|
|
}
|
|
|
|
.badge {
|
|
min-width: 32rpx;
|
|
height: 32rpx;
|
|
padding: 0 8rpx;
|
|
border-radius: 16rpx;
|
|
background-color: #FF4D4F;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.badge-txt {
|
|
font-size: 22rpx;
|
|
color: #fff;
|
|
}
|
|
|
|
.empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding-top: 160rpx;
|
|
}
|
|
|
|
.empty-icon {
|
|
font-size: 80rpx;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.empty-text {
|
|
font-size: 26rpx;
|
|
color: #999;
|
|
}
|
|
</style>
|