This commit is contained in:
oneao committed 2025-12-29 22:15:40 +08:00
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;