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

@@ -19,5 +19,8 @@ public interface ImFriendMapper extends BaseMapper<ImFriend> {
@Param("id") Long id
);
Long selectFriendTotal(@Param("userId") Long userId);
Long selectFriendTotal(
@Param("userId") Long userId,
@Param("name") String name
);
}
@@ -1,9 +1,24 @@
package cn.link56.im.mapper;
import cn.link56.im.model.po.ImGroup;
import cn.link56.im.model.vo.QueryFriendVO;
import cn.link56.im.model.vo.QueryGroupVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ImGroupMapper extends BaseMapper<ImGroup> {
List<QueryGroupVO> selectGroup(
@Param("offset") int offset,
@Param("limit") int limit,
@Param("userId") Long userId,
@Param("name") String name,
@Param("id") Long id
);
Long selectGroupTotal(@Param("userId") Long userId,
@Param("name") String name);
}
@@ -22,8 +22,8 @@ public class ImGroupUser {
@TableField("b_join_time")
private Date joinTime;
@TableField("b_last_active_time")
private Date lastActiveTime;
@TableField("b_last_message_id")
private Integer lastMessageId;
@TableField("b_unread_count")
private Integer unreadCount;
@@ -10,8 +10,6 @@ import java.util.Date;
@AllArgsConstructor
@NoArgsConstructor
public class QueryFriendVO {
private Long id;
private Long friendId;
private String friendName;
private String friendAvatar;
@@ -8,9 +8,9 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
public class QueryGroupVO {
private Long id;
private String name;
private String avatar;
private Long groupId;
private String groupName;
private String groupAvatar;
private Integer unreadCount;
@@ -63,7 +63,7 @@ public class ImFriendService extends ServiceImpl<ImFriendMapper, ImFriend> {
item.setIsOnline(SessionManager.isOnline(friendId));
});
// 查询总数
Long total = imFriendMapper.selectFriendTotal(userId);
Long total = imFriendMapper.selectFriendTotal(userId,name);
BasePageVO<QueryFriendVO> basePageVO = new BasePageVO<>();
basePageVO.setRecords(queryFriendVOS);
@@ -1,14 +1,17 @@
package cn.link56.im.service;
import cn.link56.common.BasePageVO;
import cn.link56.common.context.UserContext;
import cn.link56.im.mapper.ImGroupMapper;
import cn.link56.im.model.dto.QueryGroupDTO;
import cn.link56.im.model.po.ImGroup;
import cn.link56.im.model.vo.QueryFriendVO;
import cn.link56.im.model.vo.QueryGroupVO;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class ImGroupService extends ServiceImpl<ImGroupMapper, ImGroup>{
@@ -16,13 +19,17 @@ public class ImGroupService extends ServiceImpl<ImGroupMapper, ImGroup>{
private ImGroupMapper imGroupMapper;
public BasePageVO<QueryGroupVO> queryGroup(QueryGroupDTO dto) {
Long userId = UserContext.getUserId();
String name = dto.getName();
Long id = dto.getId();
int offset = (dto.getCurrentPage() - 1) * dto.getPageSize() + 1;
int limit = dto.getCurrentPage() * dto.getPageSize();
List<QueryGroupVO> queryFriendVOS = imGroupMapper.selectGroup(offset, limit, userId, name, id);
Long total = imGroupMapper.selectGroupTotal(userId, name);
return new BasePageVO<>();
return new BasePageVO<>(total,queryFriendVOS);
}
}
@@ -9,34 +9,41 @@
SELECT *
FROM (
SELECT
f.b_id AS id,
f.b_friend_id AS friendId,
u.b_name AS friendName,
u.b_avatar AS friendAvatar,
f.b_unread_count AS unreadCount,
m.b_id AS lastMessageId,
m.b_send_time AS lastMessageTime,
m.b_type AS lastMessageType,
m.b_content AS lastMessageContent,
ROW_NUMBER() OVER (
ORDER BY
<if test="id != null and id != 0">
CASE WHEN f.b_id = #{id} THEN 0 ELSE 1 END,
</if>
m.b_send_time DESC
) AS rn
f.b_id AS id,
f.b_friend_id AS friendId,
u.b_name AS friendName,
u.b_avatar AS friendAvatar,
f.b_unread_count AS unreadCount,
m.b_id AS lastMessageId,
m.b_send_time AS lastMessageTime,
m.b_type AS lastMessageType,
m.b_content AS lastMessageContent,
ROW_NUMBER() OVER (
ORDER BY
<if test="id != null and id != 0">
CASE WHEN f.b_id = #{id} THEN 0 ELSE 1 END,
</if>
m.b_send_time DESC
) AS rn
FROM b_im_friend f
LEFT JOIN b_company_user u ON f.b_friend_id = u.b_id
LEFT JOIN b_im_private_message m ON f.b_last_message_id = m.b_id
WHERE f.b_user_id = #{userId}
) AS t
<if test="name != null and name != ''">
AND u.b_name LIKE '%' + #{name} + '%'
</if>
) t
WHERE t.rn BETWEEN #{offset} AND #{limit}
</select>
<select id="selectFriendTotal" resultType="java.lang.Long">
SELECT COUNT(*)
FROM b_im_friend f
LEFT JOIN b_company_user u ON f.b_friend_id = u.b_id
WHERE f.b_user_id = #{userId}
<if test="name != null and name != ''">
AND u.b_name LIKE '%' + #{name} + '%'
</if>
</select>
</mapper>
@@ -9,35 +9,48 @@
SELECT *
FROM (
SELECT
g.b_id AS id,
g.b_name AS name,
g.b_avatar AS avatar,
g.b_id AS groupId,
g.b_name AS groupName,
g.b_avatar AS groupAvatar,
gu.b_unread_count AS unreadCount,
m.b_id AS lastMessageId,
m.b_send_time AS lastMessageTime,
m.b_type AS lastMessageType,
m.b_content AS lastMessageContent,
ROW_NUMBER() OVER (
ORDER BY
u.b_id AS lastMessageSenderId,
u.b_name AS lastMessageSenderName,
u.b_avatar AS lastMessageSenderAvatar,
ROW_NUMBER() OVER (
ORDER BY
<if test="id != null and id != 0">
CASE WHEN g.b_id = #{id} THEN 0 ELSE 1 END,
</if>
m.b_send_time DESC
) AS rn
FROM b_im_group g
INNER JOIN b_im_group_user gu ON g.b_id = gu.b_group_id
LEFT JOIN b_im_group_message m ON gu.b_last_message_id = m.b_id
WHERE gu.b_user_id = #{userId}
) AS t
m.b_send_time DESC
) AS rn
FROM b_im_group g
INNER JOIN b_im_group_user gu ON g.b_id = gu.b_group_id
LEFT JOIN b_im_group_message m ON gu.b_last_message_id = m.b_id
LEFT JOIN b_company_user u ON m.b_sender_id = u.b_id
WHERE gu.b_user_id = #{userId}
<if test="name != null and name != ''">
AND g.b_name LIKE '%' + #{name} + '%'
</if>
) t
WHERE t.rn BETWEEN #{offset} AND #{limit}
</select>
<select id="selectGroupTotal" resultType="java.lang.Long">
SELECT COUNT(DISTINCT g.b_id)
FROM b_im_group g
INNER JOIN b_im_group_user gu ON g.b_id = gu.b_group_id
INNER JOIN b_im_group_user gu ON g.b_id = gu.b_group_id
WHERE gu.b_user_id = #{userId}
<if test="name != null and name != ''">
AND g.b_name LIKE '%' + #{name} + '%'
</if>
</select>
</mapper>
@@ -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;