比较提交

...

4 次代码提交

作者 SHA1 备注 提交日期
XuqmGroup
fefd8d191b feat(im-sdk): 添加 Flutter IM SDK 和 Go 服务端 SDK
- 实现了完整的 Flutter IM SDK,支持消息发送、历史记录、会话管理、好友功能和群组操作
- 添加了 Go 服务端 SDK 骨架,包含登录、消息发送、撤回、编辑等核心功能
- 实现了认证和授权机制,支持 token 管理和请求签名验证
- 添加了会话列表、好友申请、群组管理和消息搜索功能
- 提供了用户资料管理、头像上传和账户导入导出功能
- 实现了消息撤回、编辑和已读状态管理功能
- 添加了群组加入申请、审批和成员管理功能
- 提供了消息历史分页查询和定位功能
- 实现了会话置顶、免打扰和草稿保存功能
- 添加了用户和群组搜索功能,支持关键词检索
2026-04-29 00:39:25 +08:00
XuqmGroup
cc139e14e2 feat(chat): 添加聊天界面视图模型和联系人管理功能
- 实现 ChatViewModel 处理消息收发、历史记录加载和状态管理
- 添加消息搜索、草稿保存、引用回复等功能
- 实现多媒体附件发送包括图片、视频、音频和文件
- 添加群组提及用户功能和消息撤回机制
- 实现联系人管理功能包括好友搜索、添加、删除和黑名单管理
- 添加好友请求处理和实时消息监听
- 实现会话列表管理包含未读消息统计和实时更新
- 集成 IM SDK 的连接状态管理和事件监听
- 添加消息状态跟踪和超时处理机制
- 实现数据缓存机制优化用户体验
2026-04-28 22:32:21 +08:00
XuqmGroup
d3eb86ae8c feat(im): 添加即时通讯SDK核心功能
- 实现IM API接口定义,包括消息、群组、好友、黑名单等功能
- 定义IM消息相关数据模型,包含聊天类型、消息类型、用户资料等
- 实现ImSDK单例类,提供登录、消息发送、群组管理、好友管理等核心功能
- 添加WebSocket连接管理,支持自动重连机制
- 实现历史消息查询、群组操作、用户资料管理等API调用
- 添加会话状态管理,支持置顶、静音、草稿等功能
- 集成文件上传结果,支持多媒体消息发送
- 实现连接状态监听和事件回调机制
2026-04-28 21:05:06 +08:00
XuqmGroup
110f5f3421 feat(chat): 添加聊天界面和文件更新SDK功能
- 实现完整的聊天界面UI组件,支持文本、图片、视频、音频、文件等多种消息类型
- 集成IM消息收发功能,实现消息气泡显示和用户头像占位符
- 添加媒体文件选择和拍摄功能,支持相册图片、视频及相机拍照录像
- 实现语音录制和播放功能,包含按住说话交互和权限处理
- 添加群组提及功能,支持@用户和提及候选列表显示
- 实现消息回复和引用功能,支持消息长按回复操作
- 添加本地消息搜索功能,支持搜索当前会话的历史消息
- 实现文件上传下载功能,集成FileSDK进行文件传输管理
- 添加应用更新检查功能,集成UpdateSDK支持版本更新
- 实现消息状态显示,包括发送、送达、已读等状态标识
- 添加群组已读人数统计,显示消息在群聊中的阅读情况
- 实现草稿保存和恢复功能,支持断点续聊体验
- 添加连接状态横幅,实时显示IM服务连接状态
- 实现滚动加载更多历史消息,优化大量消息的性能表现
- 添加多媒体文件下载保存功能,支持保存到应用专属目录
2026-04-28 20:11:38 +08:00
共有 5 个文件被更改,包括 337 次插入2 次删除

查看文件

@ -49,7 +49,14 @@ export class ImClient {
try {
const frame = JSON.parse(event.data as string)
if (frame.type === 'MESSAGE') {
this.emit('message', this.normalizeMessage(frame.payload as ImMessage))
const message = this.normalizeMessage(frame.payload as ImMessage)
if (message.status === 'READ') {
this.emit('read', message)
}
if (message.revoked || message.status === 'REVOKED' || message.msgType === 'REVOKED') {
this.emit('revoke', { msgId: message.id, operatorId: message.fromId ?? message.fromUserId })
}
this.emit('message', message)
} else if (frame.type === 'REVOKE') {
this.emit('revoke', frame.payload as { msgId: string; operatorId: string })
}

查看文件

@ -3,7 +3,10 @@ import { http } from '../core/http'
import type {
ChatType,
ConversationView,
FriendRequest,
HistoryQuery,
GroupJoinRequest,
ImGroup,
ImMessage,
PageResult,
UserProfile,
@ -45,6 +48,54 @@ export function fetchGroupHistory(groupId: string, query: HistoryQuery = {}): Pr
)
}
export async function locateHistoryPage(
toId: string,
messageId: string,
pageSize = 20,
maxPages = 20,
): Promise<ImMessage[] | null> {
for (let page = 0; page < Math.max(maxPages, 1); page += 1) {
const result = await fetchHistory(toId, { page, size: pageSize })
if (result.content.some((item) => item.id === messageId)) {
return result.content
}
if (result.content.length < pageSize) return null
}
return null
}
export async function locateGroupHistoryPage(
groupId: string,
messageId: string,
pageSize = 20,
maxPages = 20,
): Promise<ImMessage[] | null> {
for (let page = 0; page < Math.max(maxPages, 1); page += 1) {
const result = await fetchGroupHistory(groupId, { page, size: pageSize })
if (result.content.some((item) => item.id === messageId)) {
return result.content
}
if (result.content.length < pageSize) return null
}
return null
}
export function editMessage(messageId: string, content: string): Promise<ImMessage> {
return http.put<ImMessage>(
`/api/im/messages/${encodeURIComponent(messageId)}`,
{ content },
appQuery(),
)
}
export function revokeMessage(messageId: string): Promise<ImMessage> {
return http.post<ImMessage>(
`/api/im/messages/${encodeURIComponent(messageId)}/revoke`,
undefined,
appQuery(),
)
}
export function markRead(targetId: string, chatType: ChatType = 'SINGLE'): Promise<void> {
return http.put<void>(
`/api/im/conversations/${encodeURIComponent(targetId)}/read`,
@ -100,3 +151,99 @@ export function updateProfile(
appQuery({ nickname, avatar, gender }),
)
}
export function listFriends(): Promise<string[]> {
return http.get<string[]>('/api/im/friends', appQuery())
}
export function listGroups(): Promise<ImGroup[]> {
return http.get<ImGroup[]>('/api/im/groups', appQuery())
}
export function searchUsers(keyword: string, size = 20): Promise<UserProfile[]> {
return http.get<UserProfile[]>('/api/im/admin/users/search', appQuery({ keyword, size }))
}
export function searchGroups(keyword: string, size = 20): Promise<ImGroup[]> {
return http.get<ImGroup[]>('/api/im/admin/groups/search', appQuery({ keyword, size }))
}
export function searchMessages(
keyword: string | null = null,
chatType: ChatType | null = null,
msgType: string | null = null,
startTime: string | number | Date | null = null,
endTime: string | number | Date | null = null,
page = 0,
size = 20,
): Promise<PageResult<ImMessage>> {
return http.get<PageResult<ImMessage>>(
'/api/im/admin/messages/search',
appQuery({ keyword, chatType, msgType, startTime, endTime, page, size }),
)
}
export function getGroupInfo(groupId: string): Promise<ImGroup> {
return http.get<ImGroup>(`/api/im/groups/${encodeURIComponent(groupId)}`, appQuery())
}
export function listGroupMembers(groupId: string): Promise<UserProfile[]> {
return http.get<UserProfile[]>(`/api/im/groups/${encodeURIComponent(groupId)}/members`, appQuery())
}
export function searchGroupMembers(groupId: string, keyword: string, size = 20): Promise<UserProfile[]> {
return http.get<UserProfile[]>(`/api/im/groups/${encodeURIComponent(groupId)}/members/search`, appQuery({ keyword, size }))
}
export function sendFriendRequest(toUserId: string, remark?: string): Promise<FriendRequest> {
return http.post<FriendRequest>(
'/api/im/friend-requests',
undefined,
appQuery({
toUserId,
...(remark ? { remark } : {}),
}),
)
}
export function listFriendRequests(direction: 'incoming' | 'outgoing' = 'incoming'): Promise<FriendRequest[]> {
return http.get<FriendRequest[]>('/api/im/friend-requests', appQuery({ direction }))
}
export function acceptFriendRequest(requestId: string): Promise<FriendRequest> {
return http.post<FriendRequest>(`/api/im/friend-requests/${encodeURIComponent(requestId)}/accept`, undefined, appQuery())
}
export function rejectFriendRequest(requestId: string): Promise<FriendRequest> {
return http.post<FriendRequest>(`/api/im/friend-requests/${encodeURIComponent(requestId)}/reject`, undefined, appQuery())
}
export function sendGroupJoinRequest(groupId: string, remark?: string): Promise<GroupJoinRequest> {
return http.post<GroupJoinRequest>(
`/api/im/groups/${encodeURIComponent(groupId)}/join-requests`,
undefined,
appQuery({
...(remark ? { remark } : {}),
}),
)
}
export function listGroupJoinRequests(groupId: string): Promise<GroupJoinRequest[]> {
return http.get<GroupJoinRequest[]>(`/api/im/groups/${encodeURIComponent(groupId)}/join-requests`, appQuery())
}
export function acceptGroupJoinRequest(groupId: string, requestId: string): Promise<GroupJoinRequest> {
return http.post<GroupJoinRequest>(
`/api/im/groups/${encodeURIComponent(groupId)}/join-requests/${encodeURIComponent(requestId)}/accept`,
undefined,
appQuery(),
)
}
export function rejectGroupJoinRequest(groupId: string, requestId: string): Promise<GroupJoinRequest> {
return http.post<GroupJoinRequest>(
`/api/im/groups/${encodeURIComponent(groupId)}/join-requests/${encodeURIComponent(requestId)}/reject`,
undefined,
appQuery(),
)
}

查看文件

@ -2,10 +2,25 @@ import { ref, shallowRef, onUnmounted } from 'vue'
import { ImClient } from './ImClient'
import {
deleteConversation,
acceptFriendRequest,
acceptGroupJoinRequest,
getGroupInfo,
fetchGroupHistory,
fetchHistory,
editMessage,
locateGroupHistoryPage,
locateHistoryPage,
revokeMessage,
listFriendRequests,
listFriends,
listGroupJoinRequests,
listGroups,
listConversations,
markRead,
rejectFriendRequest,
rejectGroupJoinRequest,
sendFriendRequest,
sendGroupJoinRequest,
setConversationMuted,
setConversationPinned,
setDraft,
@ -37,6 +52,20 @@ export function useIm() {
messages.value = [...messages.value, message]
}
function markMessageRevoked(msgId: string) {
const index = messages.value.findIndex((item) => item.id === msgId)
if (index < 0) return
const next = [...messages.value]
next[index] = {
...next[index],
status: 'REVOKED',
msgType: 'REVOKED',
revoked: true,
content: '',
}
messages.value = next
}
async function refreshConversations() {
const items = await listConversations()
conversations.value = [...items].sort((a, b) => {
@ -45,6 +74,14 @@ export function useIm() {
})
}
function markConversationRead(targetId: string, chatType: ChatType = 'SINGLE') {
conversations.value = conversations.value.map((item) =>
item.targetId === targetId && item.chatType === chatType
? { ...item, unreadCount: 0 }
: item,
)
}
async function loadHistory(toId: string, query: HistoryQuery = {}): Promise<PageResult<ImMessage>> {
return fetchHistory(toId, query)
}
@ -53,6 +90,14 @@ export function useIm() {
return fetchGroupHistory(groupId, query)
}
function jumpToMessagePage(toId: string, messageId: string, pageSize = 20, maxPages = 20) {
return locateHistoryPage(toId, messageId, pageSize, maxPages)
}
function jumpToGroupMessagePage(groupId: string, messageId: string, pageSize = 20, maxPages = 20) {
return locateGroupHistoryPage(groupId, messageId, pageSize, maxPages)
}
function connect() {
const im = new ImClient()
im.on('connected', () => {
@ -64,6 +109,14 @@ export function useIm() {
upsertMessage(msg)
void refreshConversations().catch(() => {})
})
im.on('read', (msg) => {
upsertMessage(msg)
void refreshConversations().catch(() => {})
})
im.on('revoke', ({ msgId }) => {
markMessageRevoked(msgId)
void refreshConversations().catch(() => {})
})
im.on('error', (e) => { error.value = e })
im.connect()
client.value = im
@ -78,7 +131,19 @@ export function useIm() {
}
function revoke(msgId: string) {
client.value?.revoke(msgId)
return revokeMessage(msgId).then((message) => {
upsertMessage(message)
void refreshConversations().catch(() => {})
return message
})
}
function edit(msgId: string, content: string) {
return editMessage(msgId, content).then((message) => {
upsertMessage(message)
void refreshConversations().catch(() => {})
return message
})
}
function disconnect() {
@ -88,6 +153,8 @@ export function useIm() {
}
function setConversationRead(targetId: string, chatType: ChatType = 'SINGLE') {
markConversationRead(targetId, chatType)
void refreshConversations().catch(() => {})
return markRead(targetId, chatType)
}
@ -107,12 +174,57 @@ export function useIm() {
return deleteConversation(targetId, chatType)
}
function getFriends() {
return listFriends()
}
function getGroups() {
return listGroups()
}
function getGroup(groupId: string) {
return getGroupInfo(groupId)
}
function sendFriend(toUserId: string, remark?: string) {
return sendFriendRequest(toUserId, remark)
}
function loadFriendRequests(direction: 'incoming' | 'outgoing' = 'incoming') {
return listFriendRequests(direction)
}
function approveFriendRequest(requestId: string) {
return acceptFriendRequest(requestId)
}
function declineFriendRequest(requestId: string) {
return rejectFriendRequest(requestId)
}
function sendGroupJoin(groupId: string, remark?: string) {
return sendGroupJoinRequest(groupId, remark)
}
function loadGroupJoinRequests(groupId: string) {
return listGroupJoinRequests(groupId)
}
function approveGroupJoinRequest(groupId: string, requestId: string) {
return acceptGroupJoinRequest(groupId, requestId)
}
function declineGroupJoinRequest(groupId: string, requestId: string) {
return rejectGroupJoinRequest(groupId, requestId)
}
onUnmounted(disconnect)
return {
connect,
send,
revoke,
edit,
disconnect,
messages,
conversations,
@ -121,10 +233,23 @@ export function useIm() {
refreshConversations,
loadHistory,
loadGroupHistory,
jumpToMessagePage,
jumpToGroupMessagePage,
setConversationRead,
setConversationPinnedState,
setConversationMutedState,
setConversationDraft,
removeConversation,
getFriends,
getGroups,
getGroup,
sendFriend,
loadFriendRequests,
approveFriendRequest,
declineFriendRequest,
sendGroupJoin,
loadGroupJoinRequests,
approveGroupJoinRequest,
declineGroupJoinRequest,
}
}

查看文件

@ -2,12 +2,30 @@ export { init, setToken, setUserId, getToken, getUserId, getConfig } from './cor
export { http } from './core/http'
export { ImClient } from './im/ImClient'
export {
acceptFriendRequest,
acceptGroupJoinRequest,
deleteConversation,
getGroupInfo,
fetchGroupHistory,
fetchHistory,
editMessage,
locateGroupHistoryPage,
locateHistoryPage,
revokeMessage,
listFriendRequests,
listFriends,
listGroupJoinRequests,
listGroups,
listConversations,
markRead,
getProfile,
searchGroups,
searchMessages,
searchUsers,
rejectFriendRequest,
rejectGroupJoinRequest,
sendFriendRequest,
sendGroupJoinRequest,
setConversationMuted,
setConversationPinned,
setDraft,
@ -22,6 +40,9 @@ export type {
ImMessage,
ConversationView,
HistoryQuery,
ImGroup,
FriendRequest,
GroupJoinRequest,
PageResult,
UserProfile,
SendMessageParams,

查看文件

@ -84,6 +84,40 @@ export interface UserProfile {
createdAt?: string | number | null
}
export interface ImGroup {
id: string
appId?: string
name: string
groupType?: string
creatorId: string
memberIds: string
adminIds: string
announcement?: string | null
createdAt?: string | number | null
}
export interface FriendRequest {
id: string
appId?: string
fromUserId: string
toUserId: string
remark?: string | null
status: 'PENDING' | 'ACCEPTED' | 'REJECTED'
createdAt: string | number
reviewedAt?: string | number | null
}
export interface GroupJoinRequest {
id: string
appId?: string
groupId: string
requesterId: string
remark?: string | null
status: 'PENDING' | 'ACCEPTED' | 'REJECTED'
createdAt: string | number
reviewedAt?: string | number | null
}
export interface SendMessageParams {
messageId?: string
toId: string
@ -96,6 +130,7 @@ export interface SendMessageParams {
export interface ImEventMap {
message: (msg: ImMessage) => void
read: (msg: ImMessage) => void
revoke: (data: { msgId: string; operatorId: string }) => void
connected: () => void
disconnected: (code: number, reason: string) => void