u
This commit is contained in:
1 parent
6fd3bf5138
commit
28678fb4df
13 files changed
+358
-136
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import type { CreateFriendParams, ImFriend, QueryFriendParams } from '@/types/im'
|
||||
import type { CreateFriendParams, ImFriend, ImGroup, QueryFriendParams, QueryGroupParams } from '@/types/im'
|
||||
import { httpPostJson } from '@/request/utils'
|
||||
|
||||
export function createFriendApi(params: CreateFriendParams) {
|
||||
@@ -8,3 +8,7 @@ export function createFriendApi(params: CreateFriendParams) {
|
||||
export function queryFriendApi(params: QueryFriendParams) {
|
||||
return httpPostJson<BasePageResult<ImFriend>>('/im/friend/query', params)
|
||||
}
|
||||
|
||||
export function queryGroupApi(params: QueryGroupParams) {
|
||||
return httpPostJson<BasePageResult<ImGroup>>('/im/group/query', params)
|
||||
}
|
||||
@@ -40,7 +40,7 @@ const router = createRouter({
|
||||
component: () => import('@/views/company/detail.vue'),
|
||||
},
|
||||
{
|
||||
path: '/im',
|
||||
path: '/im/:type?/:id?',
|
||||
component: () => import('@/views/im/index.vue'),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -62,3 +62,24 @@ export interface ImFriend {
|
||||
|
||||
isOnline: boolean
|
||||
}
|
||||
|
||||
export interface QueryGroupParams {
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
|
||||
name: string
|
||||
id: number | null
|
||||
}
|
||||
|
||||
export interface ImGroup {
|
||||
groupId: number
|
||||
groupName: string
|
||||
groupAvatar: string
|
||||
|
||||
unreadCount: number
|
||||
|
||||
lastMessageId: number
|
||||
lastMessageTime: number
|
||||
lastMessageType: number
|
||||
lastMessageContent: string
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ImFriend } from '@/types/im'
|
||||
import type { ImFriend, ImGroup } from '@/types/im'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { queryFriendApi } from '@/request/api/im'
|
||||
import { queryFriendApi, queryGroupApi } from '@/request/api/im'
|
||||
import { useWebSocket } from '@/store/websocket'
|
||||
import { MessageType } from '@/types/im'
|
||||
import { formatImTime } from '@/utils/time'
|
||||
@@ -13,30 +12,92 @@ const route = useRoute()
|
||||
const websocket = useWebSocket()
|
||||
|
||||
// 当前聊天对象
|
||||
const activeChatTab = ref<'private' | 'group'>('private')
|
||||
const searchName = ref('')
|
||||
|
||||
const initialFriendId = ref<number | null>(
|
||||
route.params.friendId ? Number(route.params.friendId) : null,
|
||||
type ChatType = 'private' | 'group'
|
||||
|
||||
const initialChat = ref<{
|
||||
type: ChatType
|
||||
id: number
|
||||
} | null>(null)
|
||||
|
||||
type CurrentChat
|
||||
= | {
|
||||
type: 'private'
|
||||
id: number
|
||||
data?: ImFriend
|
||||
}
|
||||
| {
|
||||
type: 'group'
|
||||
id: number
|
||||
data?: ImGroup
|
||||
}
|
||||
|
||||
const currentChat = ref<CurrentChat | null>(null)
|
||||
|
||||
watch(
|
||||
() => initialChat.value,
|
||||
(chat) => {
|
||||
if (!chat)
|
||||
return
|
||||
|
||||
currentChat.value = {
|
||||
type: chat.type,
|
||||
id: chat.id,
|
||||
}
|
||||
|
||||
initialChat.value = null
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 聊天列表
|
||||
interface ImListState<T> {
|
||||
list: T[]
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
loading: boolean
|
||||
hasMore: boolean
|
||||
}
|
||||
|
||||
// 好友列表
|
||||
const friends = ref<ImFriend[]>([])
|
||||
const friendsPage = ref({
|
||||
const friendState = ref<ImListState<ImFriend>>({
|
||||
list: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
loading: false,
|
||||
hasMore: true,
|
||||
})
|
||||
// 群组列表
|
||||
const groupState = ref<ImListState<ImGroup>>({
|
||||
list: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
loading: false,
|
||||
hasMore: true,
|
||||
})
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
const type = route.params.type as ChatType | undefined
|
||||
const id = route.params.id ? Number(route.params.id) : null
|
||||
|
||||
if (type && id) {
|
||||
initialChat.value = { type, id }
|
||||
}
|
||||
|
||||
loadGroups()
|
||||
loadFriends()
|
||||
|
||||
websocket.onMessage((msg) => {
|
||||
const { type, from } = msg
|
||||
|
||||
// 更新在线状态
|
||||
const updateOnlineStatus = (isOnline: boolean) => {
|
||||
const friend = friends.value.find(item => item.friendId === from)
|
||||
const friend = friendState.value.list.find(item => item.friendId === from)
|
||||
if (friend)
|
||||
friend.isOnline = isOnline
|
||||
}
|
||||
@@ -52,57 +113,118 @@ onMounted(() => {
|
||||
|
||||
// 加载好友列表
|
||||
async function loadFriends() {
|
||||
if (friendsPage.value.loading)
|
||||
const state = friendState.value
|
||||
if (state.loading || !state.hasMore)
|
||||
return
|
||||
friendsPage.value.loading = true
|
||||
|
||||
state.loading = true
|
||||
try {
|
||||
const res = await queryFriendApi({
|
||||
currentPage: friendsPage.value.page,
|
||||
pageSize: friendsPage.value.pageSize,
|
||||
name: '',
|
||||
id: initialFriendId.value,
|
||||
currentPage: state.page,
|
||||
pageSize: state.pageSize,
|
||||
name: searchName.value,
|
||||
id: currentChat.value?.type === 'private' ? currentChat.value.id : null,
|
||||
})
|
||||
|
||||
if (res.isSuccess) {
|
||||
const { records, total } = res.data
|
||||
if (!res.isSuccess)
|
||||
throw new Error(res.message)
|
||||
|
||||
if (friendsPage.value.page === 1) {
|
||||
friends.value = records
|
||||
}
|
||||
else {
|
||||
// 合并并去重
|
||||
const merged = friends.value.concat(records)
|
||||
const map = new Map<number, typeof merged[0]>()
|
||||
for (const f of merged) {
|
||||
map.set(f.id, f)
|
||||
}
|
||||
friends.value = Array.from(map.values())
|
||||
}
|
||||
const { records, total } = res.data
|
||||
|
||||
// 设置当前聊天对象
|
||||
if (initialFriendId.value) {
|
||||
const friend = friends.value.find(f => f.id === initialFriendId.value)
|
||||
if (friend) {
|
||||
// openPrivateChat(friend)
|
||||
}
|
||||
initialFriendId.value = null
|
||||
}
|
||||
state.list
|
||||
= state.page === 1
|
||||
? records
|
||||
: [...state.list, ...records]
|
||||
|
||||
friendsPage.value.total = total
|
||||
}
|
||||
else {
|
||||
ElMessage.error(res.message || '加载好友列表失败,请稍后再试')
|
||||
}
|
||||
state.total = total
|
||||
state.hasMore = state.list.length < total
|
||||
state.page++
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err)
|
||||
ElMessage.error('加载好友列表失败,请稍后再试')
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
finally {
|
||||
friendsPage.value.loading = false
|
||||
state.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载群组列表
|
||||
async function loadGroups() {
|
||||
const state = groupState.value
|
||||
if (state.loading || !state.hasMore)
|
||||
return
|
||||
|
||||
state.loading = true
|
||||
try {
|
||||
const res = await queryGroupApi({
|
||||
currentPage: state.page,
|
||||
pageSize: state.pageSize,
|
||||
name: searchName.value,
|
||||
id: currentChat.value?.type === 'group' ? currentChat.value.id : null,
|
||||
})
|
||||
|
||||
if (!res.isSuccess)
|
||||
throw new Error(res.message)
|
||||
|
||||
const { records, total } = res.data
|
||||
|
||||
state.list
|
||||
= state.page === 1
|
||||
? records
|
||||
: [...state.list, ...records]
|
||||
|
||||
state.total = total
|
||||
state.hasMore = state.list.length < total
|
||||
state.page++
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
finally {
|
||||
state.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetList<T>(state: ImListState<T>) {
|
||||
state.page = 1
|
||||
state.total = 0
|
||||
state.hasMore = true
|
||||
state.list = []
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
resetList(friendState.value)
|
||||
resetList(groupState.value)
|
||||
|
||||
loadFriends()
|
||||
loadGroups()
|
||||
}
|
||||
|
||||
function onPrivateChat(friend: ImFriend) {
|
||||
// 将 friend 的未读置0
|
||||
const hadUnread = friend.unreadCount > 0
|
||||
friend.unreadCount = 0
|
||||
// 设置当前聊天
|
||||
currentChat.value = {
|
||||
id: friend.friendId,
|
||||
type: 'private',
|
||||
data: friend,
|
||||
}
|
||||
// 初始化分页
|
||||
}
|
||||
|
||||
function onGroupChat(group: ImGroup) {
|
||||
// 将 friend 的未读置0
|
||||
const hadUnread = group.unreadCount > 0
|
||||
group.unreadCount = 0
|
||||
// 设置当前聊天
|
||||
currentChat.value = {
|
||||
id: group.groupId,
|
||||
type: 'group',
|
||||
data: group,
|
||||
}
|
||||
// 初始化分页
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -137,49 +259,16 @@ async function loadFriends() {
|
||||
<div class="im__body">
|
||||
<div class="im__list">
|
||||
<div class="im__list-search">
|
||||
<el-input placeholder="请根据名称搜索" :prefix-icon="Search" />
|
||||
<el-input
|
||||
v-model="searchName"
|
||||
placeholder="请根据名称搜索"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
<el-tabs v-model="activeChatTab" class="im__list-tabs">
|
||||
<el-tab-pane label="单聊" name="private" />
|
||||
<el-tab-pane label="群聊" name="group" />
|
||||
</el-tabs>
|
||||
<div class="im__list-item">
|
||||
<div class="im__list-body">
|
||||
<div
|
||||
v-for="item in friends" :key="item.id" class="im__chat-item"
|
||||
>
|
||||
<el-badge
|
||||
is-dot
|
||||
:color="item.isOnline ? '#24e68a' : '#bfbfbf'"
|
||||
:badge-style="{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
}"
|
||||
:offset="[-4, 34]"
|
||||
>
|
||||
<SafeImage class="im__chat-item-avatar" :url="item.friendAvatar" :size="40" round />
|
||||
</el-badge>
|
||||
<div class="im__chat-item-info">
|
||||
<div class="im__chat-item-main">
|
||||
<div class="im__chat-item-name">
|
||||
{{ item.friendName }}
|
||||
</div>
|
||||
<div class="im__chat-item-last-time">
|
||||
{{ formatImTime(item.lastMessageTime, 'list') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="im__chat-item-message">
|
||||
<div class="im__chat-item-last-message">
|
||||
{{ item.lastMessageContent }}
|
||||
</div>
|
||||
<el-badge :value="item.unreadCount || 0" :show-zero="false" :max="99" class="im__chat-item-unread-count" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="im__list-item">
|
||||
<div class="im__list-header">
|
||||
<SvgIcon icon="tabler:message-circle" :size="16" />
|
||||
<span class="im__list-header-title">
|
||||
@@ -188,7 +277,12 @@ async function loadFriends() {
|
||||
</div>
|
||||
<div class="im__list-body">
|
||||
<div
|
||||
v-for="item in friends" :key="item.id" class="im__chat-item"
|
||||
v-for="item in friendState.list" :key="item.id" class="im__chat-item"
|
||||
:class="{
|
||||
'im__chat-item--selected':
|
||||
currentChat?.type === 'private' && item.friendId === currentChat.id,
|
||||
}"
|
||||
@click="onPrivateChat(item)"
|
||||
>
|
||||
<el-badge
|
||||
is-dot
|
||||
@@ -199,7 +293,7 @@ async function loadFriends() {
|
||||
}"
|
||||
:offset="[-4, 34]"
|
||||
>
|
||||
<SafeImage class="im__chat-item-avatar" :url="item.friendAvatar" :size="40" round />
|
||||
<SafeImage :size="40" :url="item.friendAvatar" round class="im__chat-item-avatar" />
|
||||
</el-badge>
|
||||
<div class="im__chat-item-info">
|
||||
<div class="im__chat-item-main">
|
||||
@@ -220,7 +314,68 @@ async function loadFriends() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="im__list-item">
|
||||
<div class="im__list-header">
|
||||
<SvgIcon icon="bi:people" :size="16" />
|
||||
<span class="im__list-header-title">
|
||||
群聊
|
||||
</span>
|
||||
</div>
|
||||
<div class="im__list-body">
|
||||
<div
|
||||
v-for="item in groupState.list" :key="item.groupId" class="im__chat-item"
|
||||
>
|
||||
<SafeImage :size="40" :url="item.groupAvatar" round class="im__chat-item-avatar" />
|
||||
|
||||
<div class="im__chat-item-info">
|
||||
<div class="im__chat-item-main">
|
||||
<div class="im__chat-item-name">
|
||||
{{ item.groupName }}
|
||||
</div>
|
||||
<div class="im__chat-item-last-time">
|
||||
{{ formatImTime(item.lastMessageTime, 'list') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="im__chat-item-message">
|
||||
<div v-if="item.lastMessageId" class="im__chat-item-last-message">
|
||||
<!-- {{ item. }}: {{ item.lastMessageContent }} -->
|
||||
</div>
|
||||
|
||||
<el-badge :value="item.unreadCount || 0" :show-zero="false" :max="99" class="im__chat-item-unread-count" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentChat" class="im__window">
|
||||
<div class="im__window-header">
|
||||
<div class="im__window-peer">
|
||||
<div class="im__window-peer-info">
|
||||
<div class="im__window-peer-name">
|
||||
{{ currentChat.type === 'private' ? currentChat.data?.friendName : currentChat.data?.groupName }}
|
||||
</div>
|
||||
<div v-if="currentChat.type === 'private'" class="im__window-peer-desc">
|
||||
<div
|
||||
class="im__window-peer-status"
|
||||
:class="currentChat.data?.isOnline ? 'im__window-peer-status--online' : 'im__window-peer-status--offline'"
|
||||
>
|
||||
<span>[ </span>
|
||||
<div class="im__window-peer-status-dot" />
|
||||
<span>{{ currentChat.data?.isOnline ? '在线' : '离线' }}</span>
|
||||
<span> ]</span>
|
||||
</div>
|
||||
<span>
|
||||
公司描述等等等...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="im__window-actions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,8 +413,7 @@ async function loadFriends() {
|
||||
|
||||
.im__container {
|
||||
width: 1300px;
|
||||
height: 80%;
|
||||
min-height: 700px;
|
||||
min-height: 750px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
|
||||
Reference in new issue
Block a user