feat(im):antd 的 im 迁移进一步对齐

pull/367/head
YunaiV 2026-06-17 22:14:05 -07:00
parent 0929ab9409
commit 24813f00f5
124 changed files with 2047 additions and 1882 deletions

View File

@ -70,8 +70,5 @@
"vue3-print-nb": "catalog:", "vue3-print-nb": "catalog:",
"vue3-signature": "catalog:", "vue3-signature": "catalog:",
"vuedraggable": "catalog:" "vuedraggable": "catalog:"
},
"devDependencies": {
"vite": "catalog:"
} }
} }

View File

@ -1,19 +1,24 @@
// TODO @AIapi 的风格,要和 vben 保持一致; import { requestClient } from '#/api/request';
import { requestClient } from '#/api/request'
// 用户端能看到的频道素材详情 export namespace ImChannelMaterialApi {
export interface ImChannelMaterialRespVO { /** 用户端能看到的频道素材详情 */
id: number export interface Material {
channelId: number id: number;
type: number channelId: number;
title: string type: number;
coverUrl?: string title: string;
summary?: string coverUrl?: string;
content?: string summary?: string;
url?: string content?: string;
url?: string;
}
} }
// 获取频道素材详情;用于客户端点击图文卡片渲染详情页
export const getChannelMaterial = (id: number) => { /** 获取频道素材详情;用于客户端点击图文卡片渲染详情页 */
return requestClient.get<ImChannelMaterialRespVO>('/im/channel/material/get', { params: { id } }) export function getChannelMaterial(id: number) {
return requestClient.get<ImChannelMaterialApi.Material>(
'/im/channel/material/get',
{ params: { id } },
);
} }

View File

@ -1,19 +1,25 @@
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
// IM 会话读位置 Response VO export namespace ImConversationReadApi {
export interface ImConversationReadRespVO { /** IM 会话读位置 Response VO */
id: number // 读位置编号(增量拉取游标用) export interface ConversationReadRespVO {
conversationType: number // 会话类型,参见 ImConversationType id: number; // 读位置编号(增量拉取游标用)
targetId: number // 会话目标编号 conversationType: number; // 会话类型,参见 ImConversationType
messageId: number // 最大已读消息编号 targetId: number; // 会话目标编号
updateTime?: number // 最近更新时间(毫秒时间戳,增量拉取游标用) messageId: number; // 最大已读消息编号
updateTime?: number; // 最近更新时间(毫秒时间戳,增量拉取游标用)
}
} }
// 增量拉取当前用户的会话读位置(重连 / 离线补偿)
export const pullMyConversationReadList = (params: { /** 增量拉取当前用户的会话读位置(重连 / 离线补偿) */
lastId?: number export function pullMyConversationReadList(params: {
lastUpdateTime?: number lastId?: number;
limit: number lastUpdateTime?: number;
}) => { limit: number;
return requestClient.get<ImConversationReadRespVO[]>('/im/conversation-read/pull', { params }) }) {
return requestClient.get<ImConversationReadApi.ConversationReadRespVO[]>(
'/im/conversation-read/pull',
{ params },
);
} }

View File

@ -1,23 +1,26 @@
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
// 用户端表情包项(精简版) export namespace ImFacePackApi {
export interface ImFacePackUserItemVO { /** 用户端表情包项(精简版) */
id: number export interface FacePackUserItem {
url: string id: number;
name?: string url: string;
width: number name?: string;
height: number width: number;
height: number;
}
/** 用户端表情包 + 嵌套 items */
export interface FacePackUser {
id: number;
name: string;
icon?: string;
items: FacePackUserItem[];
}
} }
// 用户端表情包 + 嵌套 items
export interface ImFacePackUserVO {
id: number
name: string
icon?: string
items: ImFacePackUserItemVO[]
}
// 拉取所有启用的系统表情包(含表情列表) /** 拉取所有启用的系统表情包(含表情列表) */
export const getFacePackList = () => { export function getFacePackList() {
return requestClient.get<ImFacePackUserVO[]>('/im/face-pack/list') return requestClient.get<ImFacePackApi.FacePackUser[]>('/im/face-pack/list');
} }

View File

@ -1,33 +1,38 @@
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
// 个人表情 export namespace ImFaceUserItemApi {
export interface ImFaceUserItemVO { /** 个人表情 */
id: number export interface FaceUserItem {
url: string id: number;
name?: string url: string;
width: number name?: string;
height: number width: number;
height: number;
}
/** 添加个人表情请求 */
export interface FaceUserItemSaveReqVO {
url: string;
name?: string;
width: number;
height: number;
}
} }
// 添加个人表情请求
export interface ImFaceUserItemSaveReqVO { /** 获取我的个人表情列表 */
url: string export function getFaceUserItemList() {
name?: string return requestClient.get<ImFaceUserItemApi.FaceUserItem[]>('/im/face-user-item/list');
width: number
height: number
} }
// 获取我的个人表情列表 /** 添加个人表情 */
export const getFaceUserItemList = () => { export function createFaceUserItem(data: ImFaceUserItemApi.FaceUserItemSaveReqVO) {
return requestClient.get<ImFaceUserItemVO[]>('/im/face-user-item/list') return requestClient.post<number>('/im/face-user-item/create', data);
} }
// 添加个人表情 /** 删除个人表情 */
export const createFaceUserItem = (data: ImFaceUserItemSaveReqVO) => { export function deleteFaceUserItem(id: number) {
return requestClient.post<number>('/im/face-user-item/create', data) return requestClient.delete<boolean>('/im/face-user-item/delete', {
} params: { id },
});
// 删除个人表情
export const deleteFaceUserItem = (id: number) => {
return requestClient.delete('/im/face-user-item/delete', { params: { id } })
} }

View File

@ -1,65 +1,78 @@
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
// IM 好友 Response VO export namespace ImFriendApi {
export interface ImFriendRespVO { /** IM 好友 Response VO */
id: number // 关系记录编号 export interface FriendRespVO {
friendUserId: number // 好友的用户编号 id: number; // 关系记录编号
silent?: boolean // 是否免打扰 friendUserId: number; // 好友的用户编号
displayName?: string // 好友展示备注(仅自己可见) silent?: boolean; // 是否免打扰
displayNamePinyin?: string // 备注的拼音(小写无空格,前端按首字母分桶 / 拼音搜索) displayName?: string; // 好友展示备注(仅自己可见)
addSource?: number // 添加来源;参见 ImFriendAddSourceEnum displayNamePinyin?: string; // 备注的拼音(小写无空格,前端按首字母分桶 / 拼音搜索)
pinned?: boolean // 是否置顶联系人 addSource?: number; // 添加来源;参见 ImFriendAddSourceEnum
blocked?: boolean // 是否拉黑 pinned?: boolean; // 是否置顶联系人
status?: number // 好友状态0=正常1=已删除) blocked?: boolean; // 是否拉黑
addTime?: string // 添加好友时间 status?: number; // 好友状态0=正常1=已删除)
deleteTime?: string // 删除好友时间 addTime?: string; // 添加好友时间
updateTime?: number // 最近更新时间(毫秒时间戳,增量拉取游标用) deleteTime?: string; // 删除好友时间
// 聚合字段(自 AdminUser updateTime?: number; // 最近更新时间(毫秒时间戳,增量拉取游标用)
nickname?: string // 好友昵称 nickname?: string; // 好友昵称
nicknamePinyin?: string // 昵称的拼音(小写无空格,前端按首字母分桶 / 拼音搜索) nicknamePinyin?: string; // 昵称的拼音(小写无空格,前端按首字母分桶 / 拼音搜索)
avatar?: string // 好友头像 avatar?: string; // 好友头像
}
/** IM 好友更新 Request VO */
export interface FriendUpdateReqVO {
friendUserId: number; // 好友的用户编号
silent?: boolean; // 是否免打扰
displayName?: string; // 好友展示备注
pinned?: boolean; // 是否置顶联系人
}
} }
// IM 好友更新 Request VO
export interface ImFriendUpdateReqVO { /** 获得当前登录用户的好友列表 */
friendUserId: number // 好友的用户编号 export function getMyFriendList() {
silent?: boolean // 是否免打扰 return requestClient.get<ImFriendApi.FriendRespVO[]>('/im/friend/list');
displayName?: string // 好友展示备注
pinned?: boolean // 是否置顶联系人
} }
// 获得当前登录用户的好友列表 /** 增量拉取当前用户的好友关系(重连 / 离线补偿) */
export const getMyFriendList = () => { export function pullMyFriendList(params: {
return requestClient.get<ImFriendRespVO[]>('/im/friend/list') lastId?: number;
lastUpdateTime?: number;
limit: number;
}) {
return requestClient.get<ImFriendApi.FriendRespVO[]>('/im/friend/pull', { params });
} }
// 增量拉取当前用户的好友关系(重连 / 离线补偿) /** 获得好友详情 */
export const pullMyFriendList = (params: { lastId?: number; lastUpdateTime?: number; limit: number }) => { export function getFriend(friendUserId: number | string) {
return requestClient.get<ImFriendRespVO[]>('/im/friend/pull', { params }) return requestClient.get<ImFriendApi.FriendRespVO>('/im/friend/get', {
params: { friendUserId },
});
} }
// 获得好友详情 /** 删除好友(单向软删除) */
export const getFriend = (friendUserId: number | string) => { export function deleteFriend(friendUserId: number | string, clear: boolean) {
return requestClient.get<ImFriendRespVO>('/im/friend/get', { params: { friendUserId } }) return requestClient.delete<boolean>('/im/friend/delete', {
params: { friendUserId, clear },
});
} }
// 删除好友(单向软删除) /** 更新好友信息(备注 / 免打扰 / 联系人置顶) */
export const deleteFriend = (friendUserId: number | string, clear: boolean) => { export function updateFriend(data: ImFriendApi.FriendUpdateReqVO) {
return requestClient.delete<boolean>('/im/friend/delete', { params: { friendUserId, clear } }) return requestClient.put<boolean>('/im/friend/update', data);
} }
// 更新好友信息(备注 / 免打扰 / 联系人置顶) /** 拉黑好友(必须先是好友;单边屏蔽对方私聊消息) */
export const updateFriend = (data: ImFriendUpdateReqVO) => { export function blockFriend(friendUserId: number | string) {
return requestClient.put<boolean>('/im/friend/update', data) return requestClient.put<boolean>('/im/friend/block', undefined, {
params: { friendUserId },
});
} }
// 拉黑好友(必须先是好友;单边屏蔽对方私聊消息) /** 移出黑名单 */
export const blockFriend = (friendUserId: number | string) => { export function unblockFriend(friendUserId: number | string) {
return requestClient.put<boolean>('/im/friend/block', undefined, { params: { friendUserId } }) return requestClient.put<boolean>('/im/friend/unblock', undefined, {
params: { friendUserId },
});
} }
// 移出黑名单
export const unblockFriend = (friendUserId: number | string) => {
return requestClient.put<boolean>('/im/friend/unblock', undefined, { params: { friendUserId } })
}

View File

@ -1,66 +1,84 @@
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
// IM 好友申请 Response VO export namespace ImFriendRequestApi {
export interface ImFriendRequestRespVO { /** IM 好友申请 Response VO */
id: number // 申请编号 export interface FriendRequestRespVO {
fromUserId: number // 发起方用户编号 id: number; // 申请编号
toUserId: number // 接收方用户编号 fromUserId: number; // 发起方用户编号
handleResult: number // 处理结果0=未处理1=同意2=拒绝 toUserId: number; // 接收方用户编号
applyContent?: string // 申请理由 handleResult: number; // 处理结果0=未处理1=同意2=拒绝
handleContent?: string // 处理理由(接收方拒绝时可选填) applyContent?: string; // 申请理由
addSource?: number // 添加来源;参见 ImFriendAddSourceEnum handleContent?: string; // 处理理由(接收方拒绝时可选填)
handleTime?: string // 处理时间 addSource?: number; // 添加来源;参见 ImFriendAddSourceEnum
createTime: string // 申请创建时间 handleTime?: string; // 处理时间
updateTime?: number // 最近更新时间(毫秒时间戳,增量拉取游标用) createTime: string; // 申请创建时间
// 聚合字段(自 AdminUser updateTime?: number; // 最近更新时间(毫秒时间戳,增量拉取游标用)
fromNickname?: string // 发起方昵称 fromNickname?: string; // 发起方昵称
fromAvatar?: string // 发起方头像 fromAvatar?: string; // 发起方头像
toNickname?: string // 接收方昵称 toNickname?: string; // 接收方昵称
toAvatar?: string // 接收方头像 toAvatar?: string; // 接收方头像
} }
// IM 好友申请发起 Request VO /** IM 好友申请发起 Request VO */
export interface ImFriendRequestApplyReqVO { export interface FriendRequestApplyReqVO {
toUserId: number // 接收方用户编号 toUserId: number; // 接收方用户编号
applyContent?: string // 申请理由 applyContent?: string; // 申请理由
displayName?: string // 对接收方的备注(仅自己可见) displayName?: string; // 对接收方的备注(仅自己可见)
addSource?: number // 添加来源 addSource?: number; // 添加来源
}
// 发起好友申请
export const applyFriendRequest = (data: ImFriendRequestApplyReqVO) => {
return requestClient.post<null | number>('/im/friend-request/apply', data)
}
// 同意好友申请
export const agreeFriendRequest = (id: number | string) => {
return requestClient.put<boolean>('/im/friend-request/agree', undefined, { params: { id } })
}
// 拒绝好友申请
export const refuseFriendRequest = (id: number | string, handleContent?: string) => {
return requestClient.put<boolean>('/im/friend-request/refuse', undefined, { params: { id, handleContent } })
}
// 查询「我相关」的好友申请列表(游标分页:传 maxId 加载更多)
export const getMyFriendRequestList = (limit: number, maxId?: number) => {
const params: Record<string, number> = { limit }
if (maxId != null) {
params.maxId = maxId
} }
return requestClient.get<ImFriendRequestRespVO[]>('/im/friend-request/list', { params })
} }
// 增量拉取「我相关」的好友申请变更(重连 / 离线补偿)
export const pullMyFriendRequestList = (params: { /** 发起好友申请 */
lastId?: number export function applyFriendRequest(data: ImFriendRequestApi.FriendRequestApplyReqVO) {
lastUpdateTime?: number return requestClient.post<null | number>('/im/friend-request/apply', data);
limit: number
}) => {
return requestClient.get<ImFriendRequestRespVO[]>('/im/friend-request/pull', { params })
} }
// 按 id 单查「我相关」的申请记录带越权过滤WebSocket 通知到达后用) /** 同意好友申请 */
export const getMyFriendRequest = (id: number) => { export function agreeFriendRequest(id: number | string) {
return requestClient.get<ImFriendRequestRespVO | null>('/im/friend-request/get', { params: { id } }) return requestClient.put<boolean>('/im/friend-request/agree', undefined, {
params: { id },
});
}
/** 拒绝好友申请 */
export function refuseFriendRequest(
id: number | string,
handleContent?: string,
) {
return requestClient.put<boolean>('/im/friend-request/refuse', undefined, {
params: { id, handleContent },
});
}
/** 查询「我相关」的好友申请列表(游标分页:传 maxId 加载更多) */
export function getMyFriendRequestList(limit: number, maxId?: number) {
const params: Record<string, number> = { limit };
if (maxId != null) {
params.maxId = maxId;
}
return requestClient.get<ImFriendRequestApi.FriendRequestRespVO[]>(
'/im/friend-request/list',
{ params },
);
}
/** 增量拉取「我相关」的好友申请变更(重连 / 离线补偿) */
export function pullMyFriendRequestList(params: {
lastId?: number;
lastUpdateTime?: number;
limit: number;
}) {
return requestClient.get<ImFriendRequestApi.FriendRequestRespVO[]>(
'/im/friend-request/pull',
{ params },
);
}
/** 按 id 单查「我相关」的申请记录带越权过滤WebSocket 通知到达后用) */
export function getMyFriendRequest(id: number) {
return requestClient.get<ImFriendRequestApi.FriendRequestRespVO | null>(
'/im/friend-request/get',
{ params: { id } },
);
} }

View File

@ -1,139 +1,144 @@
import type { ImGroupMessageRespVO } from '#/api/im/message/group' import type { ImGroupMessageApi } from '#/api/im/message/group';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
// 群 Response VO export namespace ImGroupApi {
export interface ImGroupRespVO { /** 群 Response VO */
id: number // 编号 export interface GroupRespVO {
name: string // 群名称 id: number; // 编号
ownerUserId: number // 群主用户编号 name: string; // 群名称
avatar?: string // 群头像 ownerUserId: number; // 群主用户编号
notice?: string // 群公告 avatar?: string; // 群头像
banned?: boolean // 是否封禁 notice?: string; // 群公告
mutedAll?: boolean // 是否全群禁言 banned?: boolean; // 是否封禁
joinApproval?: boolean // 进群是否需群主 / 管理员审批 mutedAll?: boolean; // 是否全群禁言
bannedTime?: string // 封禁时间 joinApproval?: boolean; // 进群是否需群主 / 管理员审批
status: number // 群状态0=正常1=已解散) bannedTime?: string; // 封禁时间
dissolvedTime?: string // 解散时间 status: number; // 群状态0=正常1=已解散)
createTime?: string // 创建时间 dissolvedTime?: string; // 解散时间
pinnedMessages?: ImGroupMessageRespVO[] // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空) createTime?: string; // 创建时间
joinStatus?: number // 当前登录用户在该群的成员状态(参见 CommonStatusEnum0 在群 / 1 已退群);历史退群群仍返回,供展示离线消息的群名 / 头像 pinnedMessages?: ImGroupMessageApi.GroupMessageRespVO[]; // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空)
joinStatus?: number; // 当前登录用户在该群的成员状态(参见 CommonStatusEnum0 在群 / 1 已退群);历史退群群仍返回,供展示离线消息的群名 / 头像
}
/** 群消息置顶 / 取消置顶 Request VO */
export interface GroupMessagePinReqVO {
id: number; // 群编号
messageId: number; // 消息编号
}
/** 群创建 Request VO */
export interface GroupCreateReqVO {
name: string; // 群名称
memberUserIds?: number[]; // 初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)
joinApproval?: boolean; // 进群是否需审批;不传默认 false 自由进群
}
/** 群更新 Request VO */
export interface GroupUpdateReqVO {
id: number; // 群编号
name?: string; // 群名称
avatar?: string; // 群头像
notice?: string; // 群公告
joinApproval?: boolean; // 进群是否需审批
}
/** 添加 / 撤销群管理员 Request VO */
export interface GroupAdminReqVO {
id: number; // 群编号
userIds: number[]; // 目标用户编号列表
}
/** 群主转让 Request VO */
export interface GroupTransferOwnerReqVO {
id: number; // 群编号
newOwnerUserId: number; // 新群主用户编号
}
/** 全群禁言 / 取消 Request VO */
export interface GroupMuteAllReqVO {
id: number; // 群编号
mutedAll: boolean; // 是否全群禁言
}
/** 成员禁言 Request VO */
export interface GroupMuteMemberReqVO {
id: number; // 群编号
userId: number; // 被禁言的用户编号
mutedSeconds: number; // 禁言时长0 表示永久禁言
}
/** 取消成员禁言 Request VO */
export interface GroupCancelMuteMemberReqVO {
id: number; // 群编号
userId: number; // 被取消禁言的用户编号
}
} }
// 群消息置顶 / 取消置顶 Request VO
export interface ImGroupMessagePinReqVO { /** 获得当前登录用户的群列表 */
id: number // 群编号 export function getMyGroupList() {
messageId: number // 消息编号 return requestClient.get<ImGroupApi.GroupRespVO[]>('/im/group/list');
} }
// 群创建 Request VO /** 获得群详情 */
export interface ImGroupCreateReqVO { export function getGroup(id: number | string) {
name: string // 群名称 return requestClient.get<ImGroupApi.GroupRespVO>('/im/group/get', { params: { id } });
memberUserIds?: number[] // 初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)
joinApproval?: boolean // 进群是否需审批;不传默认 false 自由进群
} }
// 群更新 Request VO /** 创建群 */
export interface ImGroupUpdateReqVO { export function createGroup(data: ImGroupApi.GroupCreateReqVO) {
id: number // 群编号 return requestClient.post<ImGroupApi.GroupRespVO>('/im/group/create', data);
name?: string // 群名称
avatar?: string // 群头像
notice?: string // 群公告
joinApproval?: boolean // 进群是否需审批
} }
// 添加 / 撤销群管理员 Request VO /** 更新群 */
export interface ImGroupAdminReqVO { export function updateGroup(data: ImGroupApi.GroupUpdateReqVO) {
id: number // 群编号 return requestClient.put<ImGroupApi.GroupRespVO>('/im/group/update', data);
userIds: number[] // 目标用户编号列表
} }
// 群主转让 Request VO /** 解散群 */
export interface ImGroupTransferOwnerReqVO { export function dissolveGroup(id: number | string) {
id: number // 群编号 return requestClient.delete<boolean>('/im/group/dissolve', {
newOwnerUserId: number // 新群主用户编号 params: { id },
});
} }
// 全群禁言 / 取消 Request VO /** 添加群管理员(仅群主可调) */
export interface ImGroupMuteAllReqVO { export function addGroupAdmin(data: ImGroupApi.GroupAdminReqVO) {
id: number // 群编号 return requestClient.put<boolean>('/im/group/add-admin', data);
mutedAll: boolean // 是否全群禁言
} }
// 成员禁言 Request VO /** 撤销群管理员(仅群主可调) */
export interface ImGroupMuteMemberReqVO { export function removeGroupAdmin(data: ImGroupApi.GroupAdminReqVO) {
id: number // 群编号 return requestClient.put<boolean>('/im/group/remove-admin', data);
userId: number // 被禁言的用户编号
mutedSeconds: number // 禁言时长0 表示永久禁言
} }
// 取消成员禁言 Request VO /** 转让群主(仅老群主可调;旧群主转让后降为普通成员) */
export interface ImGroupCancelMuteMemberReqVO { export function transferGroupOwner(data: ImGroupApi.GroupTransferOwnerReqVO) {
id: number // 群编号 return requestClient.put<boolean>('/im/group/transfer-owner', data);
userId: number // 被取消禁言的用户编号
} }
// 获得当前登录用户的群列表 /** 置顶群消息(仅群主 / 管理员可调) */
export const getMyGroupList = () => { export function pinGroupMessage(data: ImGroupApi.GroupMessagePinReqVO) {
return requestClient.get<ImGroupRespVO[]>('/im/group/list') return requestClient.put<boolean>('/im/group/pin-message', data);
} }
// 获得群详情 /** 取消置顶群消息(仅群主 / 管理员可调) */
export const getGroup = (id: number | string) => { export function unpinGroupMessage(data: ImGroupApi.GroupMessagePinReqVO) {
return requestClient.get<ImGroupRespVO>('/im/group/get', { params: { id } }) return requestClient.put<boolean>('/im/group/unpin-message', data);
} }
// 创建群 /** 全群禁言 / 取消(仅群主 / 管理员可调) */
export const createGroup = (data: ImGroupCreateReqVO) => { export function muteAll(data: ImGroupApi.GroupMuteAllReqVO) {
return requestClient.post<ImGroupRespVO>('/im/group/create', data) return requestClient.put<boolean>('/im/group/mute-all', data);
} }
// 更新群 /** 禁言成员 */
export const updateGroup = (data: ImGroupUpdateReqVO) => { export function muteMember(data: ImGroupApi.GroupMuteMemberReqVO) {
return requestClient.put<ImGroupRespVO>('/im/group/update', data) return requestClient.put<boolean>('/im/group/mute-member', data);
} }
// 解散群 /** 取消成员禁言 */
export const dissolveGroup = (id: number | string) => { export function cancelMuteMember(data: ImGroupApi.GroupCancelMuteMemberReqVO) {
return requestClient.delete<boolean>('/im/group/dissolve', { params: { id } }) return requestClient.put<boolean>('/im/group/cancel-mute-member', data);
}
// 添加群管理员(仅群主可调)
export const addGroupAdmin = (data: ImGroupAdminReqVO) => {
return requestClient.put<boolean>('/im/group/add-admin', data)
}
// 撤销群管理员(仅群主可调)
export const removeGroupAdmin = (data: ImGroupAdminReqVO) => {
return requestClient.put<boolean>('/im/group/remove-admin', data)
}
// 转让群主(仅老群主可调;旧群主转让后降为普通成员)
export const transferGroupOwner = (data: ImGroupTransferOwnerReqVO) => {
return requestClient.put<boolean>('/im/group/transfer-owner', data)
}
// 置顶群消息(仅群主 / 管理员可调)
export const pinGroupMessage = (data: ImGroupMessagePinReqVO) => {
return requestClient.put<boolean>('/im/group/pin-message', data)
}
// 取消置顶群消息(仅群主 / 管理员可调)
export const unpinGroupMessage = (data: ImGroupMessagePinReqVO) => {
return requestClient.put<boolean>('/im/group/unpin-message', data)
}
// 全群禁言 / 取消(仅群主 / 管理员可调)
export const muteAll = (data: ImGroupMuteAllReqVO) => {
return requestClient.put<boolean>('/im/group/mute-all', data)
}
// 禁言成员
export const muteMember = (data: ImGroupMuteMemberReqVO) => {
return requestClient.put<boolean>('/im/group/mute-member', data)
}
// 取消成员禁言
export const cancelMuteMember = (data: ImGroupCancelMuteMemberReqVO) => {
return requestClient.put<boolean>('/im/group/cancel-mute-member', data)
} }

View File

@ -1,70 +1,78 @@
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
// 群成员 Response VO export namespace ImGroupMemberApi {
export interface ImGroupMemberRespVO { /** 群成员 Response VO */
id: number // 编号 export interface GroupMemberRespVO {
groupId: number // 群编号 id: number; // 编号
userId: number // 用户编号 groupId: number; // 群编号
displayUserName?: string // 组内显示名(群主设置的备注) userId: number; // 用户编号
groupRemark?: string // 群备注(当前用户对群的备注) displayUserName?: string; // 组内显示名(群主设置的备注)
silent?: boolean // 是否免打扰 groupRemark?: string; // 群备注(当前用户对群的备注)
status?: number // 成员状态0=在群1=退群) silent?: boolean; // 是否免打扰
role?: number // 成员角色,参见 ImGroupMemberRole 枚举 status?: number; // 成员状态0=在群1=退群)
joinTime?: string // 入群时间 role?: number; // 成员角色,参见 ImGroupMemberRole 枚举
quitTime?: string // 退群时间 joinTime?: string; // 入群时间
muteEndTime?: string // 禁言到期时间 quitTime?: string; // 退群时间
createTime?: string // 创建时间 muteEndTime?: string; // 禁言到期时间
// 聚合字段(自 AdminUser createTime?: string; // 创建时间
nickname?: string // 用户昵称 nickname?: string; // 用户昵称
avatar?: string // 用户头像 avatar?: string; // 用户头像
}
/** 群成员邀请 Request VO */
export interface GroupMemberInviteReqVO {
groupId: number; // 群编号
memberUserIds: number[]; // 被邀请的用户编号列表
}
/** 群成员移除 Request VO */
export interface GroupMemberRemoveReqVO {
groupId: number; // 群编号
memberUserIds: number[]; // 被移除的用户编号列表
}
/** 群成员更新 Request VO */
export interface GroupMemberUpdateReqVO {
groupId: number; // 群编号
displayUserName?: string; // 群内昵称
groupRemark?: string; // 群备注
silent?: boolean; // 是否免打扰
}
} }
// 群成员邀请 Request VO
export interface ImGroupMemberInviteReqVO { /** 邀请用户加入群 */
groupId: number // 群编号 export function inviteGroupMember(data: ImGroupMemberApi.GroupMemberInviteReqVO) {
memberUserIds: number[] // 被邀请的用户编号列表 return requestClient.post<boolean>('/im/group/invite', data);
} }
// 群成员移除 Request VO /** 退出群 */
export interface ImGroupMemberRemoveReqVO { export function quitGroup(groupId: number | string) {
groupId: number // 群编号 return requestClient.delete<boolean>('/im/group/quit', {
memberUserIds: number[] // 被移除的用户编号列表 params: { groupId },
});
} }
// 群成员更新 Request VO /** 移除群成员 */
export interface ImGroupMemberUpdateReqVO { export function removeGroupMember(data: ImGroupMemberApi.GroupMemberRemoveReqVO) {
groupId: number // 群编号 return requestClient.delete<boolean>('/im/group/kicking', { data });
displayUserName?: string // 群内昵称
groupRemark?: string // 群备注
silent?: boolean // 是否免打扰
} }
// 邀请用户加入群 /** 获得群成员详情 */
export const inviteGroupMember = (data: ImGroupMemberInviteReqVO) => { export function getGroupMember(groupId: number, userId: number) {
return requestClient.post<boolean>('/im/group/invite', data) return requestClient.get<ImGroupMemberApi.GroupMemberRespVO>('/im/group-member/get', {
params: { groupId, userId },
});
} }
// 退出群 /** 获得指定群的成员列表(聚合 AdminUser 昵称 / 头像) */
export const quitGroup = (groupId: number | string) => { export function getGroupMemberList(groupId: number | string) {
return requestClient.delete<boolean>('/im/group/quit', { params: { groupId } }) return requestClient.get<ImGroupMemberApi.GroupMemberRespVO[]>('/im/group-member/list', {
params: { groupId },
});
} }
// 移除群成员 /** 更新群成员 */
export const removeGroupMember = (data: ImGroupMemberRemoveReqVO) => { export function updateGroupMember(data: ImGroupMemberApi.GroupMemberUpdateReqVO) {
return requestClient.delete<boolean>('/im/group/kicking', { data }) return requestClient.put<boolean>('/im/group-member/update', data);
}
// 获得群成员详情
export const getGroupMember = (groupId: number, userId: number) => {
return requestClient.get<ImGroupMemberRespVO>('/im/group-member/get', { params: { groupId, userId } })
}
// 获得指定群的成员列表(聚合 AdminUser 昵称 / 头像)
export const getGroupMemberList = (groupId: number | string) => {
return requestClient.get<ImGroupMemberRespVO[]>('/im/group-member/list', { params: { groupId } })
}
// 更新群成员
export const updateGroupMember = (data: ImGroupMemberUpdateReqVO) => {
return requestClient.put<boolean>('/im/group-member/update', data)
} }

View File

@ -1,66 +1,90 @@
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
// IM 加群申请 Response VO export namespace ImGroupRequestApi {
export interface ImGroupRequestRespVO { /** IM 加群申请 Response VO */
id: number // 申请编号 export interface GroupRequestRespVO {
groupId: number // 群编号 id: number; // 申请编号
userId: number // 申请人 / 被邀请人用户编号 groupId: number; // 群编号
inviterUserId?: number // 邀请人NULL 表示用户主动申请 userId: number; // 申请人 / 被邀请人用户编号
handleResult: number // 处理结果0=未处理1=同意2=拒绝 inviterUserId?: number; // 邀请人NULL 表示用户主动申请
applyContent?: string // 申请理由 handleResult: number; // 处理结果0=未处理1=同意2=拒绝
handleContent?: string // 处理理由(拒绝时可选填) applyContent?: string; // 申请理由
handleUserId?: number // 处理人用户编号 handleContent?: string; // 处理理由(拒绝时可选填)
addSource?: number // 加入来源;参见 ImGroupAddSourceEnum handleUserId?: number; // 处理人用户编号
handleTime?: string // 处理时间 addSource?: number; // 加入来源;参见 ImGroupAddSourceEnum
createTime: string // 申请创建时间 handleTime?: string; // 处理时间
updateTime?: number // 最近更新时间(毫秒时间戳,增量拉取游标用) createTime: string; // 申请创建时间
// 聚合字段 updateTime?: number; // 最近更新时间(毫秒时间戳,增量拉取游标用)
userNickname?: string // 申请人 / 被邀请人昵称 userNickname?: string; // 申请人 / 被邀请人昵称
userAvatar?: string // 申请人 / 被邀请人头像 userAvatar?: string; // 申请人 / 被邀请人头像
inviterNickname?: string // 邀请人昵称 inviterNickname?: string; // 邀请人昵称
inviterAvatar?: string // 邀请人头像 inviterAvatar?: string; // 邀请人头像
groupName?: string // 群名称 groupName?: string; // 群名称
groupAvatar?: string // 群头像 groupAvatar?: string; // 群头像
}
/** IM 加群申请发起 Request VO */
export interface GroupRequestApplyReqVO {
groupId: number; // 群编号
applyContent?: string; // 申请理由
addSource?: number; // 加入来源
}
} }
// IM 加群申请发起 Request VO
export interface ImGroupRequestApplyReqVO { /** 申请加群 */
groupId: number // 群编号 export function applyJoinGroup(data: ImGroupRequestApi.GroupRequestApplyReqVO) {
applyContent?: string // 申请理由 return requestClient.post<null | number>('/im/group-request/apply', data);
addSource?: number // 加入来源
} }
// 申请加群 /** 同意加群申请(群主或管理员) */
export const applyJoinGroup = (data: ImGroupRequestApplyReqVO) => { export function agreeGroupRequest(id: number | string) {
return requestClient.post<null | number>('/im/group-request/apply', data) return requestClient.put<boolean>('/im/group-request/agree', undefined, {
params: { id },
});
} }
// 同意加群申请(群主或管理员) /** 拒绝加群申请(群主或管理员) */
export const agreeGroupRequest = (id: number | string) => { export function refuseGroupRequest(
return requestClient.put<boolean>('/im/group-request/agree', undefined, { params: { id } }) id: number | string,
handleContent?: string,
) {
return requestClient.put<boolean>('/im/group-request/refuse', undefined, {
params: { id, handleContent },
});
} }
// 拒绝加群申请(群主或管理员) /** 查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表 */
export const refuseGroupRequest = (id: number | string, handleContent?: string) => { export function getUnhandledRequestList() {
return requestClient.put<boolean>('/im/group-request/refuse', undefined, { params: { id, handleContent } }) return requestClient.get<ImGroupRequestApi.GroupRequestRespVO[]>(
'/im/group-request/unhandled-list',
);
} }
// 查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表 /** 查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查 */
export const getUnhandledRequestList = () => { export function getGroupRequestListByGroupId(groupId: number) {
return requestClient.get<ImGroupRequestRespVO[]>('/im/group-request/unhandled-list') return requestClient.get<ImGroupRequestApi.GroupRequestRespVO[]>(
'/im/group-request/list-by-group',
{ params: { groupId } },
);
} }
// 查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查 /** 按 id 单查申请记录带越权过滤WebSocket 通知到达后用) */
export const getGroupRequestListByGroupId = (groupId: number) => { export function getMyGroupRequest(id: number) {
return requestClient.get<ImGroupRequestRespVO[]>('/im/group-request/list-by-group', { params: { groupId } }) return requestClient.get<ImGroupRequestApi.GroupRequestRespVO | null>(
'/im/group-request/get',
{ params: { id } },
);
} }
// 按 id 单查申请记录带越权过滤WebSocket 通知到达后用) /** 增量拉取我管理的所有群下加群申请变更(重连 / 离线补偿) */
export const getMyGroupRequest = (id: number) => { export function pullMyGroupRequestList(params: {
return requestClient.get<ImGroupRequestRespVO | null>('/im/group-request/get', { params: { id } }) lastId?: number;
} lastUpdateTime?: number;
limit: number;
// 增量拉取我管理的所有群下加群申请变更(重连 / 离线补偿) }) {
export const pullMyGroupRequestList = (params: { lastId?: number; lastUpdateTime?: number; limit: number }) => { return requestClient.get<ImGroupRequestApi.GroupRequestRespVO[]>(
return requestClient.get<ImGroupRequestRespVO[]>('/im/group-request/pull', { params }) '/im/group-request/pull',
{ params },
);
} }

View File

@ -1,43 +1,56 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerChannelVO { export namespace ImManagerChannelApi {
id: number /** 频道 */
code: string export interface Channel {
name: string id: number;
avatar?: string code: string;
sort: number name: string;
status: number avatar?: string;
createTime?: Date sort: number;
status: number;
createTime?: Date;
}
} }
// 获得频道分页
export const getManagerChannelPage = (params: PageParam) => { /** 获得频道分页 */
return requestClient.get('/im/manager/channel/page', { params }) export function getManagerChannelPage(params: PageParam) {
return requestClient.get<PageResult<ImManagerChannelApi.Channel>>(
'/im/manager/channel/page',
{ params },
);
} }
// 获得频道详情 /** 获得频道详情 */
export const getManagerChannel = (id: number) => { export function getManagerChannel(id: number) {
return requestClient.get('/im/manager/channel/get', { params: { id } }) return requestClient.get<ImManagerChannelApi.Channel>('/im/manager/channel/get', {
params: { id },
});
} }
// 新增频道 /** 新增频道 */
export const createManagerChannel = (data: ImManagerChannelVO) => { export function createManagerChannel(data: ImManagerChannelApi.Channel) {
return requestClient.post('/im/manager/channel/create', data) return requestClient.post<number>('/im/manager/channel/create', data);
} }
// 修改频道 /** 修改频道 */
export const updateManagerChannel = (data: ImManagerChannelVO) => { export function updateManagerChannel(data: ImManagerChannelApi.Channel) {
return requestClient.put('/im/manager/channel/update', data) return requestClient.put<boolean>('/im/manager/channel/update', data);
} }
// 删除频道 /** 删除频道 */
export const deleteManagerChannel = (id: number) => { export function deleteManagerChannel(id: number) {
return requestClient.delete('/im/manager/channel/delete', { params: { id } }) return requestClient.delete<boolean>('/im/manager/channel/delete', {
params: { id },
});
} }
// 获得启用的频道精简列表(表单选择用) /** 获得启用的频道精简列表(表单选择用) */
export const getSimpleChannelList = () => { export function getSimpleChannelList() {
return requestClient.get<ImManagerChannelVO[]>('/im/manager/channel/simple-list') return requestClient.get<ImManagerChannelApi.Channel[]>(
'/im/manager/channel/simple-list',
);
} }

View File

@ -1,46 +1,68 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerChannelMaterialVO { export namespace ImManagerChannelMaterialApi {
id: number /** 频道素材 */
channelId: number export interface Material {
channelName?: string id: number;
type: number channelId: number;
title: string channelName?: string;
coverUrl?: string type: number;
summary?: string title: string;
content?: string coverUrl?: string;
url?: string summary?: string;
createTime?: Date content?: string;
url?: string;
createTime?: Date;
}
} }
// 获得素材分页
export const getManagerChannelMaterialPage = (params: PageParam) => { /** 获得素材分页 */
return requestClient.get('/im/manager/channel-material/page', { params }) export function getManagerChannelMaterialPage(params: PageParam) {
return requestClient.get<PageResult<ImManagerChannelMaterialApi.Material>>(
'/im/manager/channel-material/page',
{ params },
);
} }
// 获得指定频道下的素材精简列表 /** 获得指定频道下的素材精简列表 */
export const getSimpleManagerChannelMaterialList = (channelId: number) => { export function getSimpleManagerChannelMaterialList(channelId: number) {
return requestClient.get('/im/manager/channel-material/simple-list', { params: { channelId } }) return requestClient.get<ImManagerChannelMaterialApi.Material[]>(
'/im/manager/channel-material/simple-list',
{ params: { channelId } },
);
} }
// 获得素材详情 /** 获得素材详情 */
export const getManagerChannelMaterial = (id: number) => { export function getManagerChannelMaterial(id: number) {
return requestClient.get('/im/manager/channel-material/get', { params: { id } }) return requestClient.get<ImManagerChannelMaterialApi.Material>(
'/im/manager/channel-material/get',
{ params: { id } },
);
} }
// 新增素材 /** 新增素材 */
export const createManagerChannelMaterial = (data: ImManagerChannelMaterialVO) => { export function createManagerChannelMaterial(
return requestClient.post('/im/manager/channel-material/create', data) data: ImManagerChannelMaterialApi.Material,
) {
return requestClient.post<number>('/im/manager/channel-material/create', data);
} }
// 修改素材 /** 修改素材 */
export const updateManagerChannelMaterial = (data: ImManagerChannelMaterialVO) => { export function updateManagerChannelMaterial(
return requestClient.put('/im/manager/channel-material/update', data) data: ImManagerChannelMaterialApi.Material,
) {
return requestClient.put<boolean>(
'/im/manager/channel-material/update',
data,
);
} }
// 删除素材 /** 删除素材 */
export const deleteManagerChannelMaterial = (id: number) => { export function deleteManagerChannelMaterial(id: number) {
return requestClient.delete('/im/manager/channel-material/delete', { params: { id } }) return requestClient.delete<boolean>('/im/manager/channel-material/delete', {
params: { id },
});
} }

View File

@ -1,36 +1,48 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerChannelMessageVO { export namespace ImManagerChannelMessageApi {
id: number /** 频道消息 */
channelId: number export interface ChannelMessage {
channelName?: string id: number;
materialId: number channelId: number;
materialTitle?: string channelName?: string;
materialCoverUrl?: string materialId: number;
type: number materialTitle?: string;
content?: string materialCoverUrl?: string;
receiverUserIds?: number[] type: number;
sendTime?: Date content?: string;
receiverUserIds?: number[];
sendTime?: Date;
}
/** 频道消息发送请求 */
export interface ChannelMessageSendReqVO {
materialId: number;
receiverUserIds?: number[];
}
} }
export interface ImManagerChannelMessageSendReqVO {
materialId: number /** 立即推送频道消息 */
receiverUserIds?: number[] export function sendManagerChannelMessage(
data: ImManagerChannelMessageApi.ChannelMessageSendReqVO,
) {
return requestClient.post<number>('/im/manager/channel-message/send', data);
} }
// 立即推送频道消息 /** 删除频道消息 */
export const sendManagerChannelMessage = (data: ImManagerChannelMessageSendReqVO) => { export function deleteManagerChannelMessage(id: number) {
return requestClient.post('/im/manager/channel-message/send', data) return requestClient.delete<boolean>('/im/manager/channel-message/delete', {
params: { id },
});
} }
// 删除频道消息 /** 获得频道消息分页 */
export const deleteManagerChannelMessage = (id: number) => { export function getManagerChannelMessagePage(params: PageParam) {
return requestClient.delete('/im/manager/channel-message/delete', { params: { id } }) return requestClient.get<PageResult<ImManagerChannelMessageApi.ChannelMessage>>(
} '/im/manager/channel-message/page',
{ params },
// 获得频道消息分页 );
export const getManagerChannelMessagePage = (params: PageParam) => {
return requestClient.get('/im/manager/channel-message/page', { params })
} }

View File

@ -1,47 +1,63 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerFacePackItemVO { export namespace ImManagerFacePackItemApi {
id: number /** 表情项 */
packId: number export interface FacePackItem {
url: string id: number;
name?: string packId: number;
width: number url: string;
height: number name?: string;
sort: number width: number;
status: number height: number;
createTime?: Date sort: number;
status: number;
createTime?: Date;
}
} }
// 获得表情分页
export const getManagerFacePackItemPage = (params: PageParam) => { /** 获得表情分页 */
return requestClient.get('/im/manager/face-pack-item/page', { params }) export function getManagerFacePackItemPage(params: PageParam) {
return requestClient.get<PageResult<ImManagerFacePackItemApi.FacePackItem>>(
'/im/manager/face-pack-item/page',
{ params },
);
} }
// 获得表情详情 /** 获得表情详情 */
export const getManagerFacePackItem = (id: number) => { export function getManagerFacePackItem(id: number) {
return requestClient.get('/im/manager/face-pack-item/get', { params: { id } }) return requestClient.get<ImManagerFacePackItemApi.FacePackItem>(
'/im/manager/face-pack-item/get',
{ params: { id } },
);
} }
// 新增表情 /** 新增表情 */
export const createManagerFacePackItem = (data: ImManagerFacePackItemVO) => { export function createManagerFacePackItem(data: ImManagerFacePackItemApi.FacePackItem) {
return requestClient.post('/im/manager/face-pack-item/create', data) return requestClient.post<number>('/im/manager/face-pack-item/create', data);
} }
// 修改表情 /** 修改表情 */
export const updateManagerFacePackItem = (data: ImManagerFacePackItemVO) => { export function updateManagerFacePackItem(data: ImManagerFacePackItemApi.FacePackItem) {
return requestClient.put('/im/manager/face-pack-item/update', data) return requestClient.put<boolean>(
'/im/manager/face-pack-item/update',
data,
);
} }
// 删除表情 /** 删除表情 */
export const deleteManagerFacePackItem = (id: number) => { export function deleteManagerFacePackItem(id: number) {
return requestClient.delete('/im/manager/face-pack-item/delete', { params: { id } }) return requestClient.delete<boolean>('/im/manager/face-pack-item/delete', {
params: { id },
});
} }
// 批量删除表情 /** 批量删除表情 */
export const deleteManagerFacePackItemList = (ids: number[]) => { export function deleteManagerFacePackItemList(ids: number[]) {
return requestClient.delete('/im/manager/face-pack-item/delete-list', { return requestClient.delete<boolean>(
params: { ids: ids.join(',') } '/im/manager/face-pack-item/delete-list',
}) { params: { ids: ids.join(',') } },
);
} }

View File

@ -1,44 +1,55 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerFacePackVO { export namespace ImManagerFacePackApi {
id: number /** 表情包 */
name: string export interface FacePack {
icon?: string id: number;
sort: number name: string;
status: number icon?: string;
createTime?: Date sort: number;
status: number;
createTime?: Date;
}
} }
// 获得表情包分页
export const getManagerFacePackPage = (params: PageParam) => { /** 获得表情包分页 */
return requestClient.get('/im/manager/face-pack/page', { params }) export function getManagerFacePackPage(params: PageParam) {
return requestClient.get<PageResult<ImManagerFacePackApi.FacePack>>(
'/im/manager/face-pack/page',
{ params },
);
} }
// 获得表情包详情 /** 获得表情包详情 */
export const getManagerFacePack = (id: number) => { export function getManagerFacePack(id: number) {
return requestClient.get('/im/manager/face-pack/get', { params: { id } }) return requestClient.get<ImManagerFacePackApi.FacePack>('/im/manager/face-pack/get', {
params: { id },
});
} }
// 新增表情包 /** 新增表情包 */
export const createManagerFacePack = (data: ImManagerFacePackVO) => { export function createManagerFacePack(data: ImManagerFacePackApi.FacePack) {
return requestClient.post('/im/manager/face-pack/create', data) return requestClient.post<number>('/im/manager/face-pack/create', data);
} }
// 修改表情包 /** 修改表情包 */
export const updateManagerFacePack = (data: ImManagerFacePackVO) => { export function updateManagerFacePack(data: ImManagerFacePackApi.FacePack) {
return requestClient.put('/im/manager/face-pack/update', data) return requestClient.put<boolean>('/im/manager/face-pack/update', data);
} }
// 删除表情包 /** 删除表情包 */
export const deleteManagerFacePack = (id: number) => { export function deleteManagerFacePack(id: number) {
return requestClient.delete('/im/manager/face-pack/delete', { params: { id } }) return requestClient.delete<boolean>('/im/manager/face-pack/delete', {
params: { id },
});
} }
// 批量删除表情包 /** 批量删除表情包 */
export const deleteManagerFacePackList = (ids: number[]) => { export function deleteManagerFacePackList(ids: number[]) {
return requestClient.delete('/im/manager/face-pack/delete-list', { return requestClient.delete<boolean>('/im/manager/face-pack/delete-list', {
params: { ids: ids.join(',') } params: { ids: ids.join(',') },
}) });
} }

View File

@ -1,24 +1,33 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerFaceUserItemVO { export namespace ImManagerFaceUserItemApi {
id: number /** 用户表情 */
userId: number export interface FaceUserItem {
userNickname?: string id: number;
url: string userId: number;
name?: string userNickname?: string;
width?: number url: string;
height?: number name?: string;
createTime?: Date width?: number;
height?: number;
createTime?: Date;
}
} }
// 获得用户表情分页
export const getManagerFaceUserItemPage = (params: PageParam) => { /** 获得用户表情分页 */
return requestClient.get('/im/manager/face-user-item/page', { params }) export function getManagerFaceUserItemPage(params: PageParam) {
return requestClient.get<PageResult<ImManagerFaceUserItemApi.FaceUserItem>>(
'/im/manager/face-user-item/page',
{ params },
);
} }
// 删除用户表情 /** 删除用户表情 */
export const deleteManagerFaceUserItem = (id: number) => { export function deleteManagerFaceUserItem(id: number) {
return requestClient.delete('/im/manager/face-user-item/delete', { params: { id } }) return requestClient.delete<boolean>('/im/manager/face-user-item/delete', {
params: { id },
});
} }

View File

@ -1,25 +1,32 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerFriendVO { export namespace ImManagerFriendApi {
id: number /** 好友关系 */
userId: number export interface Friend {
userNickname?: string id: number;
friendUserId: number userId: number;
friendNickname?: string userNickname?: string;
displayName?: string friendUserId: number;
addSource?: number friendNickname?: string;
silent: boolean displayName?: string;
pinned: boolean addSource?: number;
blocked: boolean silent: boolean;
status: number pinned: boolean;
addTime?: Date blocked: boolean;
deleteTime?: Date status: number;
createTime: Date addTime?: Date;
deleteTime?: Date;
createTime: Date;
}
} }
// 获得好友关系分页
export const getManagerFriendPage = (params: PageParam) => { /** 获得好友关系分页 */
return requestClient.get('/im/manager/friend/page', { params }) export function getManagerFriendPage(params: PageParam) {
return requestClient.get<PageResult<ImManagerFriendApi.Friend>>(
'/im/manager/friend/page',
{ params },
);
} }

View File

@ -1,23 +1,30 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerFriendRequestVO { export namespace ImManagerFriendRequestApi {
id: number /** 好友申请 */
fromUserId: number export interface FriendRequest {
fromNickname?: string id: number;
toUserId: number fromUserId: number;
toNickname?: string fromNickname?: string;
applyContent?: string toUserId: number;
displayName?: string toNickname?: string;
addSource?: number applyContent?: string;
handleResult: number displayName?: string;
handleContent?: string addSource?: number;
handleTime?: Date handleResult: number;
createTime: Date handleContent?: string;
handleTime?: Date;
createTime: Date;
}
} }
// 获得好友申请分页
export const getManagerFriendRequestPage = (params: PageParam) => { /** 获得好友申请分页 */
return requestClient.get('/im/manager/friend-request/page', { params }) export function getManagerFriendRequestPage(params: PageParam) {
return requestClient.get<PageResult<ImManagerFriendRequestApi.FriendRequest>>(
'/im/manager/friend-request/page',
{ params },
);
} }

View File

@ -1,64 +1,87 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerGroupVO { export namespace ImManagerGroupApi {
id: number /** 群 */
name: string export interface Group {
avatar?: string id: number;
notice?: string name: string;
ownerUserId: number avatar?: string;
ownerNickname?: string notice?: string;
memberCount?: number ownerUserId: number;
status: number ownerNickname?: string;
banned: boolean memberCount?: number;
mutedAll?: boolean status: number;
bannedReason?: string banned: boolean;
bannedTime?: Date mutedAll?: boolean; // 是否全群禁言
dissolvedTime?: Date bannedReason?: string;
createTime: Date bannedTime?: Date;
dissolvedTime?: Date;
createTime: Date;
}
/** 群成员 */
export interface GroupMember {
userId: number;
nickname?: string;
avatar?: string;
displayUserName?: string;
groupRemark?: string;
silent?: boolean;
status: number;
role?: number; // 成员角色,参见 ImGroupMemberRole 枚举
joinTime?: Date;
quitTime?: Date;
muteEndTime?: Date; // 禁言到期时间
}
/** 群封禁请求 */
export interface GroupBanReqVO {
id: number;
reason: string;
}
} }
export interface ImManagerGroupMemberVO {
userId: number /** 获得群分页 */
nickname?: string export function getManagerGroupPage(params: PageParam) {
avatar?: string return requestClient.get<PageResult<ImManagerGroupApi.Group>>(
displayUserName?: string '/im/manager/group/page',
groupRemark?: string { params },
silent?: boolean );
status: number
role?: number
joinTime?: Date
quitTime?: Date
muteEndTime?: Date
} }
// 获得群分页 /** 获得群详情 */
export const getManagerGroupPage = (params: PageParam) => { export function getManagerGroup(id: number) {
return requestClient.get('/im/manager/group/page', { params }) return requestClient.get<ImManagerGroupApi.Group>('/im/manager/group/get', {
params: { id },
});
} }
// 获得群详情 /** 封禁群 */
export const getManagerGroup = (id: number) => { export function banManagerGroup(data: ImManagerGroupApi.GroupBanReqVO) {
return requestClient.get('/im/manager/group/get', { params: { id } }) return requestClient.put<boolean>('/im/manager/group/ban', data);
} }
// 封禁群 /** 解封群 */
export const banManagerGroup = (data: { id: number; reason: string }) => { export function unbanManagerGroup(id: number) {
return requestClient.put('/im/manager/group/ban', data) return requestClient.put<boolean>('/im/manager/group/unban', undefined, {
params: { id },
});
} }
// 解封群 /** 解散群 */
export const unbanManagerGroup = (id: number) => { export function dissolveManagerGroup(id: number) {
return requestClient.put('/im/manager/group/unban', undefined, { params: { id } }) return requestClient.delete<boolean>('/im/manager/group/dissolve', {
params: { id },
});
} }
// 解散群 /** 获得群成员列表(含已退群成员,由前端按需过滤) */
export const dissolveManagerGroup = (id: number) => { export function getManagerGroupMemberList(groupId: number) {
return requestClient.delete('/im/manager/group/dissolve', { params: { id } }) return requestClient.get<ImManagerGroupApi.GroupMember[]>(
} '/im/manager/group/member/list',
{ params: { groupId } },
// 获得群成员列表 );
export const getManagerGroupMemberList = (groupId: number) => {
return requestClient.get('/im/manager/group/member/list', { params: { groupId } })
} }

View File

@ -1,26 +1,33 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerGroupRequestVO { export namespace ImManagerGroupRequestApi {
id: number /** 加群申请 */
groupId: number export interface GroupRequest {
groupName?: string id: number;
userId: number groupId: number;
userNickname?: string groupName?: string;
inviterUserId?: number userId: number;
inviterNickname?: string userNickname?: string;
applyContent?: string inviterUserId?: number;
addSource?: number inviterNickname?: string;
handleResult: number applyContent?: string;
handleUserId?: number addSource?: number;
handleNickname?: string handleResult: number;
handleContent?: string handleUserId?: number;
handleTime?: Date handleNickname?: string;
createTime: Date handleContent?: string;
handleTime?: Date;
createTime: Date;
}
} }
// 获得加群申请分页
export const getManagerGroupRequestPage = (params: PageParam) => { /** 获得加群申请分页 */
return requestClient.get('/im/manager/group-request/page', { params }) export function getManagerGroupRequestPage(params: PageParam) {
return requestClient.get<PageResult<ImManagerGroupRequestApi.GroupRequest>>(
'/im/manager/group-request/page',
{ params },
);
} }

View File

@ -1,30 +1,40 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerGroupMessageVO { export namespace ImManagerGroupMessageApi {
id: number /** 群聊消息 */
clientMessageId?: string export interface GroupMessage {
groupId: number id: number;
groupName?: string clientMessageId?: string;
senderId: number groupId: number;
senderNickname?: string groupName?: string;
type: number senderId: number;
content: string senderNickname?: string;
status: number type: number;
atUserIds?: number[] content: string;
atUserNicknames?: (null | string)[] status: number;
receiptStatus: number atUserIds?: number[];
sendTime: Date atUserNicknames?: (null | string)[];
createTime: Date receiptStatus: number;
sendTime: Date;
createTime: Date;
}
} }
// 获得群聊消息分页
export const getManagerGroupMessagePage = (params: PageParam) => { /** 获得群聊消息分页 */
return requestClient.get('/im/manager/message/group/page', { params }) export function getManagerGroupMessagePage(params: PageParam) {
return requestClient.get<PageResult<ImManagerGroupMessageApi.GroupMessage>>(
'/im/manager/message/group/page',
{ params },
);
} }
// 获得群聊消息详情 /** 获得群聊消息详情 */
export const getManagerGroupMessage = (id: number) => { export function getManagerGroupMessage(id: number) {
return requestClient.get('/im/manager/message/group/get', { params: { id } }) return requestClient.get<ImManagerGroupMessageApi.GroupMessage>(
'/im/manager/message/group/get',
{ params: { id } },
);
} }

View File

@ -1,28 +1,37 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerPrivateMessageVO { export namespace ImManagerPrivateMessageApi {
id: number /** 私聊消息 */
clientMessageId?: string export interface PrivateMessage {
senderId: number id: number;
senderNickname?: string clientMessageId?: string;
receiverId: number senderId: number;
receiverNickname?: string senderNickname?: string;
type: number receiverId: number;
content: string receiverNickname?: string;
status: number type: number;
receiptStatus: number content: string;
sendTime: Date status: number;
createTime: Date sendTime: Date;
createTime: Date;
}
} }
// 获得私聊消息分页
export const getManagerPrivateMessagePage = (params: PageParam) => { /** 获得私聊消息分页 */
return requestClient.get('/im/manager/message/private/page', { params }) export function getManagerPrivateMessagePage(params: PageParam) {
return requestClient.get<PageResult<ImManagerPrivateMessageApi.PrivateMessage>>(
'/im/manager/message/private/page',
{ params },
);
} }
// 获得私聊消息详情 /** 获得私聊消息详情 */
export const getManagerPrivateMessage = (id: number) => { export function getManagerPrivateMessage(id: number) {
return requestClient.get('/im/manager/message/private/get', { params: { id } }) return requestClient.get<ImManagerPrivateMessageApi.PrivateMessage>(
'/im/manager/message/private/get',
{ params: { id } },
);
} }

View File

@ -1,42 +1,53 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerRtcCallVO { export namespace ImManagerRtcApi {
id: number /** RTC 通话 */
room: string export interface RtcCall {
conversationType: number id: number;
mediaType: number room: string;
inviterUserId: number conversationType: number;
inviterNickname?: string mediaType: number;
groupId?: number inviterUserId: number;
groupName?: string inviterNickname?: string;
status: number groupId?: number;
endReason?: number groupName?: string;
startTime: Date status: number;
acceptTime?: Date endReason?: number;
endTime?: Date startTime: Date;
createTime: Date acceptTime?: Date;
endTime?: Date;
createTime: Date;
}
/** RTC 通话参与者 */
export interface RtcParticipant {
id: number;
callId: number;
userId: number;
userNickname?: string;
role: number;
status: number;
inviteTime: Date;
acceptTime?: Date;
leaveTime?: Date;
}
} }
export interface ImManagerRtcParticipantVO {
id: number /** 获得通话记录分页 */
callId: number export function getManagerRtcCallPage(params: PageParam) {
userId: number return requestClient.get<PageResult<ImManagerRtcApi.RtcCall>>(
userNickname?: string '/im/manager/rtc/page',
role: number { params },
status: number );
inviteTime: Date
acceptTime?: Date
leaveTime?: Date
} }
// 获得通话记录分页 /** 获得通话参与者列表 */
export const getManagerRtcCallPage = (params: PageParam) => { export function getManagerRtcCallParticipantList(id: number) {
return requestClient.get('/im/manager/rtc/page', { params }) return requestClient.get<ImManagerRtcApi.RtcParticipant[]>(
} '/im/manager/rtc/participant-list',
{ params: { id } },
// 获得通话参与者列表 );
export const getManagerRtcCallParticipantList = (id: number) => {
return requestClient.get('/im/manager/rtc/participant-list', { params: { id } })
} }

View File

@ -1,44 +1,60 @@
import type { PageParam } from '@vben/request' import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImManagerSensitiveWordVO { export namespace ImManagerSensitiveWordApi {
id: number /** 敏感词 */
word: string export interface SensitiveWord {
status: number id: number;
creator?: string word: string;
creatorName?: string status: number;
createTime?: Date creator?: string;
creatorName?: string;
createTime?: Date;
}
} }
// 获得敏感词分页
export const getManagerSensitiveWordPage = (params: PageParam) => { /** 获得敏感词分页 */
return requestClient.get('/im/manager/sensitive-word/page', { params }) export function getManagerSensitiveWordPage(params: PageParam) {
return requestClient.get<PageResult<ImManagerSensitiveWordApi.SensitiveWord>>(
'/im/manager/sensitive-word/page',
{ params },
);
} }
// 获得敏感词详情 /** 获得敏感词详情 */
export const getManagerSensitiveWord = (id: number) => { export function getManagerSensitiveWord(id: number) {
return requestClient.get('/im/manager/sensitive-word/get', { params: { id } }) return requestClient.get<ImManagerSensitiveWordApi.SensitiveWord>(
'/im/manager/sensitive-word/get',
{ params: { id } },
);
} }
// 新增敏感词 /** 新增敏感词 */
export const createManagerSensitiveWord = (data: ImManagerSensitiveWordVO) => { export function createManagerSensitiveWord(data: ImManagerSensitiveWordApi.SensitiveWord) {
return requestClient.post('/im/manager/sensitive-word/create', data) return requestClient.post<number>('/im/manager/sensitive-word/create', data);
} }
// 修改敏感词 /** 修改敏感词 */
export const updateManagerSensitiveWord = (data: ImManagerSensitiveWordVO) => { export function updateManagerSensitiveWord(data: ImManagerSensitiveWordApi.SensitiveWord) {
return requestClient.put('/im/manager/sensitive-word/update', data) return requestClient.put<boolean>(
'/im/manager/sensitive-word/update',
data,
);
} }
// 删除敏感词 /** 删除敏感词 */
export const deleteManagerSensitiveWord = (id: number) => { export function deleteManagerSensitiveWord(id: number) {
return requestClient.delete('/im/manager/sensitive-word/delete', { params: { id } }) return requestClient.delete<boolean>('/im/manager/sensitive-word/delete', {
params: { id },
});
} }
// 批量删除敏感词 /** 批量删除敏感词 */
export const deleteManagerSensitiveWordList = (ids: number[]) => { export function deleteManagerSensitiveWordList(ids: number[]) {
return requestClient.delete('/im/manager/sensitive-word/delete-list', { return requestClient.delete<boolean>(
params: { ids: ids.join(',') } '/im/manager/sensitive-word/delete-list',
}) { params: { ids: ids.join(',') } },
);
} }

View File

@ -1,66 +1,88 @@
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImStatisticsOverviewVO { export namespace ImManagerStatisticsApi {
totalUser: number /** 统计概览 */
newUserToday: number export interface Overview {
totalGroup: number totalUser: number;
newGroupToday: number newUserToday: number;
activeUserDaily: number totalGroup: number;
activeUserWeekly: number newGroupToday: number;
activeUserMonthly: number activeUserDaily: number;
privateMessageToday: number activeUserWeekly: number;
groupMessageToday: number activeUserMonthly: number;
privateMessageYesterday: number privateMessageToday: number;
groupMessageYesterday: number groupMessageToday: number;
privateMessageYesterday: number;
groupMessageYesterday: number;
}
/** 趋势数据 */
export interface Trend {
dates: string[];
series: Record<string, number[]>;
}
/** 消息类型分布 */
export interface MessageType {
type: number; // 参见 ImContentTypeEnum 枚举类,由前端按 DICT_TYPE.IM_CONTENT_TYPE 翻译
value: number;
}
/** 群规模分布 */
export interface GroupSize {
range: string;
count: number;
}
/** 消息发送排行 */
export interface TopSender {
userId: number;
nickname: string;
messageCount: number;
}
} }
export interface ImStatisticsTrendVO {
dates: string[] /** 获得 KPI 概览 */
series: Record<string, number[]> export function getStatisticsOverview() {
return requestClient.get<ImManagerStatisticsApi.Overview>(
'/im/manager/statistics/overview',
);
} }
export interface ImStatisticsMessageTypeVO { /** 获得消息趋势(私聊 + 群聊双线) */
type: number export function getMessageTrend(days: number) {
value: number return requestClient.get<ImManagerStatisticsApi.Trend>(
'/im/manager/statistics/message-trend',
{ params: { days } },
);
} }
export interface ImStatisticsGroupSizeVO { /** 获得用户趋势(新增注册 + 日活双线) */
range: string export function getUserTrend(days: number) {
count: number return requestClient.get<ImManagerStatisticsApi.Trend>(
'/im/manager/statistics/user-trend',
{ params: { days } },
);
} }
export interface ImStatisticsTopSenderVO { /** 获得内容类型分布(最近 30 天) */
userId: number export function getMessageTypeDistribution() {
nickname: string return requestClient.get<ImManagerStatisticsApi.MessageType[]>(
messageCount: number '/im/manager/statistics/message-type-distribution',
);
} }
// 获得 KPI 概览 /** 获得群规模分布 */
export const getStatisticsOverview = (): Promise<ImStatisticsOverviewVO> => { export function getGroupSizeDistribution() {
return requestClient.get('/im/manager/statistics/overview') return requestClient.get<ImManagerStatisticsApi.GroupSize[]>(
'/im/manager/statistics/group-size-distribution',
);
} }
// 获得消息趋势 /** 获得消息 TOP 发送者(最近 30 天) */
export const getMessageTrend = (days: number): Promise<ImStatisticsTrendVO> => { export function getTopSenders() {
return requestClient.get('/im/manager/statistics/message-trend', { params: { days } }) return requestClient.get<ImManagerStatisticsApi.TopSender[]>(
} '/im/manager/statistics/top-senders',
);
// 获得用户趋势
export const getUserTrend = (days: number): Promise<ImStatisticsTrendVO> => {
return requestClient.get('/im/manager/statistics/user-trend', { params: { days } })
}
// 获得内容类型分布
export const getMessageTypeDistribution = (): Promise<ImStatisticsMessageTypeVO[]> => {
return requestClient.get('/im/manager/statistics/message-type-distribution')
}
// 获得群规模分布
export const getGroupSizeDistribution = (): Promise<ImStatisticsGroupSizeVO[]> => {
return requestClient.get('/im/manager/statistics/group-size-distribution')
}
// 获得消息 TOP 发送者
export const getTopSenders = (): Promise<ImStatisticsTopSenderVO[]> => {
return requestClient.get('/im/manager/statistics/top-senders')
} }

View File

@ -1,22 +1,34 @@
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
export interface ImChannelMessageRespVO { export namespace ImChannelMessageApi {
id: number /** 频道消息 */
clientMessageId?: string export interface ChannelMessageRespVO {
channelId: number id: number;
materialId: number clientMessageId?: string;
type: number channelId: number;
content: string materialId: number;
receiptStatus?: number type: number;
sendTime: string content: string;
receiptStatus?: number;
sendTime: string;
}
} }
// 拉取当前用户应收的频道消息(离线增量);按 minId 游标分页
export const pullChannelMessages = (params: { minId: number; size?: number }, signal?: AbortSignal) => { /** 拉取当前用户应收的频道消息(离线增量);按 minId 游标分页 */
return requestClient.get<ImChannelMessageRespVO[]>('/im/channel/message/pull', { params, signal }) export function pullChannelMessages(
params: { minId: number; size?: number },
signal?: AbortSignal,
) {
return requestClient.get<ImChannelMessageApi.ChannelMessageRespVO[]>(
'/im/channel/message/pull',
{ params, signal },
);
} }
// 上报频道消息已读位置;切到频道会话或拉到新消息后调 /** 上报频道消息已读位置;切到频道会话或拉到新消息后调 */
export const readChannelMessages = (channelId: number, messageId: number) => { export function readChannelMessages(channelId: number, messageId: number) {
return requestClient.put('/im/channel/message/read', undefined, { params: { channelId, messageId } }) return requestClient.put<boolean>('/im/channel/message/read', undefined, {
params: { channelId, messageId },
});
} }

View File

@ -1,70 +1,92 @@
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
// 群聊消息 Response VO export namespace ImGroupMessageApi {
export interface ImGroupMessageRespVO { /** 群聊消息 Response VO */
id: number // 消息编号 export interface GroupMessageRespVO {
clientMessageId: string // 客户端消息编号 id: number; // 消息编号
senderId: number // 发送人编号 clientMessageId: string; // 客户端消息编号
groupId: number // 群编号 senderId: number; // 发送人编号
type: number // 内容类型 groupId: number; // 群编号
content: string // 消息内容JSON 格式) type: number; // 内容类型
status: number // 消息状态 content: string; // 消息内容JSON 格式)
sendTime: string // 发送时间 status: number; // 消息状态
atUserIds?: number[] // @ 目标用户编号列表 sendTime: string; // 发送时间
receiverUserIds?: number[] // 定向接收用户编号列表 atUserIds?: number[]; // @ 目标用户编号列表
receiptStatus?: number // 回执状态 receiverUserIds?: number[]; // 定向接收用户编号列表
readCount?: number // 已读人数(回执消息、且发送人为当前用户时有值) receiptStatus?: number; // 回执状态
readCount?: number; // 已读人数(回执消息、且发送人为当前用户时有值)
}
/** 群聊消息发送 Request VO */
export interface GroupMessageSendReqVO {
clientMessageId: string; // 客户端消息编号
groupId: number; // 群编号
type: number; // 内容类型
content: string; // 消息内容JSON 格式)
atUserIds?: number[]; // @ 目标用户编号列表
receipt?: boolean; // 是否需要回执
}
/** 群聊历史消息列表 Request VO */
export interface GroupMessageListReqVO {
groupId: number | string; // 群编号
maxId?: number | string; // 起始消息编号(不含),为空则从最新消息开始
limit: number; // 拉取数量1 ~ 200
}
} }
// 群聊消息发送 Request VO
export interface ImGroupMessageSendReqVO { /** 发送群聊消息 */
clientMessageId: string // 客户端消息编号 export function sendGroupMessage(data: ImGroupMessageApi.GroupMessageSendReqVO) {
groupId: number // 群编号 return requestClient.post<ImGroupMessageApi.GroupMessageRespVO>(
type: number // 内容类型 '/im/message/group/send',
content: string // 消息内容JSON 格式) data,
atUserIds?: number[] // @ 目标用户编号列表 );
receipt?: boolean // 是否需要回执
} }
// 群聊历史消息列表 Request VO /** 拉取群聊消息(增量) */
export interface ImGroupMessageListReqVO { export function pullGroupMessages(
groupId: number | string // 群编号
maxId?: number | string // 起始消息编号(不含),为空则从最新消息开始
limit: number // 拉取数量1 ~ 200
}
// 发送群聊消息
export const sendGroupMessage = (data: ImGroupMessageSendReqVO) => {
return requestClient.post<ImGroupMessageRespVO>('/im/message/group/send', data)
}
// 拉取群聊消息(增量)
export const pullGroupMessages = (
params: { minId: number | string; size: number }, params: { minId: number | string; size: number },
signal?: AbortSignal signal?: AbortSignal,
) => { ) {
return requestClient.get<ImGroupMessageRespVO[]>('/im/message/group/pull', { params, signal }) return requestClient.get<ImGroupMessageApi.GroupMessageRespVO[]>(
'/im/message/group/pull',
{ params, signal },
);
} }
// 查询群聊历史消息 /** 查询群聊历史消息 */
export const getGroupMessageList = (params: ImGroupMessageListReqVO) => { export function getGroupMessageList(params: ImGroupMessageApi.GroupMessageListReqVO) {
return requestClient.get<ImGroupMessageRespVO[]>('/im/message/group/list', { params }) return requestClient.get<ImGroupMessageApi.GroupMessageRespVO[]>(
'/im/message/group/list',
{ params },
);
} }
// 标记群聊消息已读 /** 标记群聊消息已读 */
export const readGroupMessages = (groupId: number | string, messageId: number | string) => { export function readGroupMessages(
return requestClient.put<boolean>('/im/message/group/read', undefined, { params: { groupId, messageId } }) groupId: number | string,
messageId: number | string,
) {
return requestClient.put<boolean>('/im/message/group/read', undefined, {
params: { groupId, messageId },
});
} }
// 撤回群聊消息 /** 撤回群聊消息 */
export const recallGroupMessage = (id: number | string) => { export function recallGroupMessage(id: number | string) {
return requestClient.delete<ImGroupMessageRespVO>('/im/message/group/recall', { params: { id } }) return requestClient.delete<ImGroupMessageApi.GroupMessageRespVO>(
'/im/message/group/recall',
{ params: { id } },
);
} }
// 获取群消息已读用户列表 /** 获取群消息已读用户列表 */
export const getGroupReadUsers = (params: { export function getGroupReadUsers(params: {
groupId: number | string groupId: number | string;
messageId: number | string messageId: number | string;
}) => { }) {
return requestClient.get<number[]>('/im/message/group/get-read-user-ids', { params }) return requestClient.get<number[]>('/im/message/group/get-read-user-ids', {
params,
});
} }

View File

@ -1,63 +1,89 @@
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
// 私聊消息 Response VO export namespace ImPrivateMessageApi {
export interface ImPrivateMessageRespVO { /** 私聊消息 Response VO */
id: number // 消息编号 export interface PrivateMessageRespVO {
clientMessageId: string // 客户端消息编号 id: number; // 消息编号
senderId: number // 发送人编号 clientMessageId: string; // 客户端消息编号
receiverId: number // 接收人编号 senderId: number; // 发送人编号
type: number // 内容类型 receiverId: number; // 接收人编号
content: string // 消息内容JSON 格式) type: number; // 内容类型
status: number // 消息状态(正常 / 已撤回) content: string; // 消息内容JSON 格式)
receiptStatus?: number // 回执状态(不需要 / 待完成 / 已完成),对齐 ImMessageReceiptStatus status: number; // 消息状态(正常 / 已撤回)
sendTime: string // 发送时间 receiptStatus?: number; // 回执状态(不需要 / 待完成 / 已完成),对齐 ImMessageReceiptStatus
sendTime: string; // 发送时间
}
/** 私聊消息发送 Request VO */
export interface PrivateMessageSendReqVO {
clientMessageId: string; // 客户端消息编号
receiverId: number; // 接收人编号
type: number; // 内容类型
content: string; // 消息内容JSON 格式)
receipt?: boolean; // 是否需要回执;不传后端默认 true普通私聊用户消息
}
/** 私聊历史消息列表 Request VO */
export interface PrivateMessageListReqVO {
receiverId: number | string; // 接收人编号(对方)
maxId?: number | string; // 起始消息编号(不含),为空则从最新消息开始
limit: number; // 拉取数量1 ~ 200
}
} }
// 私聊消息发送 Request VO
export interface ImPrivateMessageSendReqVO { /** 发送私聊消息 */
clientMessageId: string // 客户端消息编号 export function sendPrivateMessage(data: ImPrivateMessageApi.PrivateMessageSendReqVO) {
receiverId: number // 接收人编号 return requestClient.post<ImPrivateMessageApi.PrivateMessageRespVO>(
type: number // 内容类型 '/im/message/private/send',
content: string // 消息内容JSON 格式) data,
receipt?: boolean // 是否需要回执;不传后端默认 true普通私聊用户消息 );
} }
// 私聊历史消息列表 Request VO /** 拉取私聊消息(增量) */
export interface ImPrivateMessageListReqVO { export function pullPrivateMessages(
receiverId: number | string // 接收人编号(对方)
maxId?: number | string // 起始消息编号(不含),为空则从最新消息开始
limit: number // 拉取数量1 ~ 200
}
// 发送私聊消息
export const sendPrivateMessage = (data: ImPrivateMessageSendReqVO) => {
return requestClient.post<ImPrivateMessageRespVO>('/im/message/private/send', data)
}
// 拉取私聊消息(增量)
export const pullPrivateMessages = (
params: { minId: number | string; size: number }, params: { minId: number | string; size: number },
signal?: AbortSignal signal?: AbortSignal,
) => { ) {
return requestClient.get<ImPrivateMessageRespVO[]>('/im/message/private/pull', { params, signal }) return requestClient.get<ImPrivateMessageApi.PrivateMessageRespVO[]>(
'/im/message/private/pull',
{ params, signal },
);
} }
// 查询私聊历史消息 /** 查询私聊历史消息 */
export const getPrivateMessageList = (params: ImPrivateMessageListReqVO) => { export function getPrivateMessageList(params: ImPrivateMessageApi.PrivateMessageListReqVO) {
return requestClient.get<ImPrivateMessageRespVO[]>('/im/message/private/list', { params }) return requestClient.get<ImPrivateMessageApi.PrivateMessageRespVO[]>(
'/im/message/private/list',
{ params },
);
} }
// 标记私聊消息已读 /** 标记私聊消息已读 */
export const readPrivateMessages = (receiverId: number | string, messageId: number | string) => { export function readPrivateMessages(
return requestClient.put<boolean>('/im/message/private/read', undefined, { params: { receiverId, messageId } }) receiverId: number | string,
messageId: number | string,
) {
return requestClient.put<boolean>('/im/message/private/read', undefined, {
params: { receiverId, messageId },
});
} }
// 查询对方已读到我发的最大消息 id多端 / 离线后用于补齐已读状态) /** 查询对方已读到我发的最大消息 id多端 / 离线后用于补齐已读状态) */
export const getPrivateMaxReadMessageId = (peerId: number | string, signal?: AbortSignal) => { export function getPrivateMaxReadMessageId(
return requestClient.get<null | number>('/im/message/private/max-read-message-id', { params: { peerId }, signal }) peerId: number | string,
signal?: AbortSignal,
) {
return requestClient.get<null | number>(
'/im/message/private/max-read-message-id',
{ params: { peerId }, signal },
);
} }
// 撤回私聊消息 /** 撤回私聊消息 */
export const recallPrivateMessage = (id: number | string) => { export function recallPrivateMessage(id: number | string) {
return requestClient.delete<ImPrivateMessageRespVO>('/im/message/private/recall', { params: { id } }) return requestClient.delete<ImPrivateMessageApi.PrivateMessageRespVO>(
'/im/message/private/recall',
{ params: { id } },
);
} }

View File

@ -1,85 +1,105 @@
import { requestClient } from '#/api/request' import { requestClient } from '#/api/request';
// 创建新通话请求 VO export namespace ImRtcApi {
export interface ImRtcCallCreateReqVO { /** 创建新通话请求 VO */
conversationType: number export interface RtcCallCreateReqVO {
mediaType: number conversationType: number;
groupId?: number mediaType: number;
inviteeIds: number[] // 被邀请的用户编号集合;私聊必传 1 个对端,群聊必传至少 1 人 groupId?: number;
inviteeIds: number[]; // 被邀请的用户编号集合;私聊必传 1 个对端,群聊必传至少 1 人
}
/** 通话中追加邀请请求 VO仅群通话可用 */
export interface RtcCallInviteReqVO {
room: string;
inviteeIds: number[];
}
/** 通话会话 VOcreate / join / accept / refreshToken 共用 */
export interface RtcCallRespVO {
room: string; // 业务通话编号(同时作为 LiveKit 房间名)
livekitUrl: string;
token?: string; // ENDED 状态时为 null无需 connect LiveKit
conversationType: number;
mediaType: number;
status: number;
endReason?: number; // 结束原因;仅 status=ENDED 时有值
inviterId: number;
groupId?: number;
inviteeIds?: number[];
joinedUserIds?: number[];
}
/** 群活跃通话查询响应;不含 token */
export interface RtcGroupCallRespVO {
room: string;
groupId: number;
mediaType: number;
inviterId: number;
joinedUserIds?: number[];
inviteeIds?: number[];
}
} }
// 通话中追加邀请请求 VO仅群通话可用
export interface ImRtcCallInviteReqVO { /** 创建新通话;私聊或群聊根据 conversationType 区分 */
room: string export function createCall(data: ImRtcApi.RtcCallCreateReqVO) {
inviteeIds: number[] return requestClient.post<ImRtcApi.RtcCallRespVO>('/im/rtc/create', data);
} }
// 通话会话 VOcreate / join / accept / refreshToken 共用 /** 通话中追加邀请;仅群通话可用 */
export interface ImRtcCallRespVO { export function inviteCall(data: ImRtcApi.RtcCallInviteReqVO) {
room: string // 业务通话编号(同时作为 LiveKit 房间名) return requestClient.post<boolean>('/im/rtc/invite', data);
livekitUrl: string
token?: string // ENDED 状态时为 null无需 connect LiveKit
conversationType: number
mediaType: number
status: number
endReason?: number // 结束原因;仅 status=ENDED 时有值
inviterId: number
groupId?: number
inviteeIds?: number[]
joinedUserIds?: number[]
} }
// 群活跃通话查询响应;不含 token /** 加入已有群通话;用于胶囊条「加入」按钮 */
export interface ImRtcGroupCallRespVO { export function joinCall(room: string) {
room: string return requestClient.post<ImRtcApi.RtcCallRespVO>('/im/rtc/join', undefined, {
groupId: number params: { room },
mediaType: number });
inviterId: number
joinedUserIds?: number[]
inviteeIds?: number[]
} }
// 创建新通话;私聊或群聊根据 conversationType 区分 /** 接听通话 */
export const createCall = (data: ImRtcCallCreateReqVO) => { export function acceptCall(room: string) {
return requestClient.post<ImRtcCallRespVO>('/im/rtc/create', data) return requestClient.post<ImRtcApi.RtcCallRespVO>('/im/rtc/accept', undefined, {
params: { room },
});
} }
// 通话中追加邀请;仅群通话可用 /** 拒绝通话 */
export const inviteCall = (data: ImRtcCallInviteReqVO) => { export function rejectCall(room: string) {
return requestClient.post<boolean>('/im/rtc/invite', data) return requestClient.post<boolean>('/im/rtc/reject', undefined, {
params: { room },
});
} }
// 加入已有群通话;用于胶囊条「加入」按钮 /** 取消邀请;主叫接通前调用 */
export const joinCall = (room: string) => { export function cancelCall(room: string) {
return requestClient.post<ImRtcCallRespVO>('/im/rtc/join', undefined, { params: { room } }) return requestClient.post<boolean>('/im/rtc/cancel', undefined, {
params: { room },
});
} }
// 接听通话 /** 离开通话;接通后调用 */
export const acceptCall = (room: string) => { export function leaveCall(room: string) {
return requestClient.post<ImRtcCallRespVO>('/im/rtc/accept', undefined, { params: { room } }) return requestClient.post<boolean>('/im/rtc/leave', undefined, {
params: { room },
});
} }
// 拒绝通话 /** 振铃超时检查RUNNING 端 timer 兜底,触发后端立即扫描该 room 的超时 INVITING接口静默 */
export const rejectCall = (room: string) => { export function noAnswerCallCheck(room: string) {
return requestClient.post<boolean>('/im/rtc/reject', undefined, { params: { room } }) return requestClient.post<boolean>(
'/im/rtc/no-answer-call-check',
undefined,
{ params: { room } },
);
} }
// 取消邀请;主叫接通前调用 /** 查询当前进行中的通话;目前仅群聊场景(胶囊条),返回 null 表示无活跃通话 */
export const cancelCall = (room: string) => { export function getActiveCall(groupId: number) {
return requestClient.post<boolean>('/im/rtc/cancel', undefined, { params: { room } }) return requestClient.get<ImRtcApi.RtcGroupCallRespVO | null>(
} '/im/rtc/get-active-call',
{ params: { groupId } },
// 离开通话;接通后调用 );
export const leaveCall = (room: string) => {
return requestClient.post<boolean>('/im/rtc/leave', undefined, { params: { room } })
}
// 振铃超时检查RUNNING 端 timer 兜底,触发后端立即扫描该 room 的超时 INVITING接口静默
export const noAnswerCallCheck = (room: string) => {
return requestClient.post<boolean>('/im/rtc/no-answer-call-check', undefined, { params: { room } })
}
// 查询当前进行中的通话;目前仅群聊场景(胶囊条),返回 null 表示无活跃通话
export const getActiveCall = (groupId: number) => {
return requestClient.get<ImRtcGroupCallRespVO | null>('/im/rtc/get-active-call', { params: { groupId } })
} }

View File

@ -9,16 +9,23 @@ export namespace SystemUserApi {
username: string; username: string;
nickname: string; nickname: string;
deptId: number; deptId: number;
deptName?: string;
postIds: string[]; postIds: string[];
email: string; email: string;
mobile: string; mobile: string;
sex: number; sex: number;
avatar: string; avatar: string;
loginIp: string; loginIp: string;
loginDate?: Date;
status: number; status: number;
remark: string; remark: string;
createTime?: Date; createTime?: Date;
} }
/** 用户精简信息 */
export interface UserSimple extends User {
id: number;
}
} }
/** 查询用户管理列表 */ /** 查询用户管理列表 */
@ -86,3 +93,17 @@ export function updateUserStatus(id: number, status: number) {
export function getSimpleUserList() { export function getSimpleUserList() {
return requestClient.get<SystemUserApi.User[]>('/system/user/simple-list'); return requestClient.get<SystemUserApi.User[]>('/system/user/simple-list');
} }
/** 按用户编号查询用户精简信息 */
export function getSimpleUser(id: number | string) {
return requestClient.get<SystemUserApi.UserSimple>('/system/user/get-simple', {
params: { id },
});
}
/** 按昵称模糊搜索用户 */
export function getSimpleUserListByNickname(nickname: string) {
return requestClient.get<SystemUserApi.UserSimple[]>('/system/user/list-by-nickname', {
params: { nickname },
});
}

View File

@ -21,18 +21,20 @@ const conversationStore = useConversationStore()
const friendStore = useFriendStore() const friendStore = useFriendStore()
const uiStore = useImUiStore() const uiStore = useImUiStore()
const totalUnread = computed(() => conversationStore.getTotalUnreadCount) // Tab /** 消息 Tab 的红点:所有非免打扰会话的未读总和 */
const unhandledRequestCount = computed(() => friendStore.getUnhandledRequestCount) // Tab = const totalUnread = computed(() => conversationStore.getTotalUnreadCount)
/** 通讯录 Tab 的红点:未处理好友申请数(接收方=我) */
const unhandledRequestCount = computed(() => friendStore.getUnhandledRequestCount)
const tabs = [ const tabs = [
{ name: 'ImHomeConversation', icon: 'ep:chat-round' }, { name: 'ImHomeConversation', icon: 'ep:chat-round' },
{ name: 'ImHomeContact', icon: 'mingcute:contacts-line' } { name: 'ImHomeContact', icon: 'mingcute:contacts-line' }
] // 两个主 Tab用路由 name 而非 path避免前缀 / 嵌套调整后失效 ] // 两个主 Tab用路由 name 而非 path避免前缀 / 嵌套调整后失效
/** 当前路由是否命中 Tab直接比对 route.name */ // Tab route.name
const isActive = (name: string) => route.name === name const isActive = (name: string) => route.name === name
/** 切换 Tab当前已选中时消息 Tab 触发"滚动到下一个未读"(对齐微信 PC其它 Tab 无动作 */ // Tab Tab "" PC Tab
const goTab = (name: string) => { const goTab = (name: string) => {
if (route.name === name) { if (route.name === name) {
if (name === 'ImHomeConversation') { if (name === 'ImHomeConversation') {
@ -43,7 +45,7 @@ const goTab = (name: string) => {
router.push({ name }) router.push({ name })
} }
/** 跳转个人中心(路由 name=Profile */ // name=Profile
const goProfile = () => router.push({ name: 'Profile' }) const goProfile = () => router.push({ name: 'Profile' })
</script> </script>

View File

@ -1,14 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { SystemUserApi } from '#/api/system/user'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { useUserStore } from '@vben/stores' import { useUserStore } from '@vben/stores'
import { Button, Input, Modal, Spin } from 'ant-design-vue' import { Button, Input, message, Modal, Spin } from 'ant-design-vue'
import { getSimpleUserListByNickname } from '#/api/system/user'
import { getCurrentUserId } from '#/views/im/utils/auth' import { getCurrentUserId } from '#/views/im/utils/auth'
import { useMessage } from '#/views/im/utils/message-feedback'
import { getSimpleUserListByNickname, type UserVO } from '#/views/im/utils/system-user'
import { ImFriendAddSource } from '../../../utils/constants' import { ImFriendAddSource } from '../../../utils/constants'
import { getGenderColor, getGenderIcon } from '../../../utils/user' import { getGenderColor, getGenderIcon } from '../../../utils/user'
@ -17,18 +18,14 @@ import UserAvatar from '../user/UserAvatar.vue'
defineOptions({ name: 'ImFriendAddDialog' }) defineOptions({ name: 'ImFriendAddDialog' })
// TODO @AI const visible = ref(false) //
const visible = ref(false) const presetUser = ref<null | SystemUserApi.UserSimple>(null) // 预填目标用户:非 null 时跳过搜索步骤,直接进入申请表单(群成员加好友 / 名片加好友等场景
/** 预填目标用户:非 null 时跳过搜索步骤,直接进入申请表单(群成员加好友 / 名片加好友等场景) */ const addSource = ref<number>(ImFriendAddSource.SEARCH) // ImFriendAddSourceEnum SEARCH
const presetUser = ref<null | UserVO>(null) const addSourceExtra = ref<string>('') // addSource=ImFriendAddSource.GROUP XX YY
/** 添加来源;参见 ImFriendAddSourceEnum默认 SEARCH */
const addSource = ref<number>(ImFriendAddSource.SEARCH)
/** 来源附带信息addSource=ImFriendAddSource.GROUP 时传群名,话术拼为「我是 XX 群的 YY」 */
const addSourceExtra = ref<string>('')
defineExpose({ defineExpose({
/** 打开加好友弹窗reset → 灌参 → visible=true不传 opts 走搜索模式 */ /** 打开加好友弹窗reset → 灌参 → visible=true不传 opts 走搜索模式 */
open(opts?: { addSource?: number; addSourceExtra?: string; presetUser?: null | UserVO; }) { open(opts?: { addSource?: number; addSourceExtra?: string; presetUser?: null | SystemUserApi.UserSimple; }) {
presetUser.value = opts?.presetUser ?? null presetUser.value = opts?.presetUser ?? null
addSource.value = opts?.addSource ?? ImFriendAddSource.SEARCH addSource.value = opts?.addSource ?? ImFriendAddSource.SEARCH
addSourceExtra.value = opts?.addSourceExtra ?? '' addSourceExtra.value = opts?.addSourceExtra ?? ''
@ -39,7 +36,6 @@ defineExpose({
const friendStore = useFriendStore() const friendStore = useFriendStore()
const userStore = useUserStore() const userStore = useUserStore()
const message = useMessage()
/** 当前登录用户编号;用 computed 包一层,切账号后随 wsCache 重取,避免顶层求值在 keep-alive 实例里持有旧 id */ /** 当前登录用户编号;用 computed 包一层,切账号后随 wsCache 重取,避免顶层求值在 keep-alive 实例里持有旧 id */
const currentUserId = computed(() => getCurrentUserId()) const currentUserId = computed(() => getCurrentUserId())
@ -48,20 +44,15 @@ const currentUserId = computed(() => getCurrentUserId())
const visibleUsers = computed(() => const visibleUsers = computed(() =>
users.value.filter((user) => user.id !== currentUserId.value) users.value.filter((user) => user.id !== currentUserId.value)
) )
const keyword = ref('') const keyword = ref('') //
const users = ref<UserVO[]>([]) const users = ref<SystemUserApi.UserSimple[]>([]) //
const searched = ref(false) const searched = ref(false) //
const loading = ref(false) const loading = ref(false) //
const step = ref<'apply' | 'search'>('search') // search=apply=
/** 当前步骤search=搜索列表apply=申请表单 */ const targetUser = ref<null | SystemUserApi.UserSimple>(null) //
const step = ref<'apply' | 'search'>('search') const applyContent = ref('') // ${}
/** 申请目标用户 */ const displayName = ref('') //
const targetUser = ref<null | UserVO>(null) const submitting = ref(false) //
/** 申请理由(默认填「我是 ${当前昵称}」,对齐微信交互) */
const applyContent = ref('')
/** 对接收方的备注(仅自己可见) */
const displayName = ref('')
const submitting = ref(false)
/** 弹窗标题随步骤切换 */ /** 弹窗标题随步骤切换 */
const dialogTitle = computed(() => (step.value === 'apply' ? '申请添加朋友' : '添加好友')) const dialogTitle = computed(() => (step.value === 'apply' ? '申请添加朋友' : '添加好友'))
@ -115,7 +106,7 @@ async function handleSearch() {
} }
/** 进入申请步骤:预填申请理由「我是 ${当前用户昵称}」(对齐微信交互) */ /** 进入申请步骤:预填申请理由「我是 ${当前用户昵称}」(对齐微信交互) */
function enterApply(user: UserVO) { function enterApply(user: SystemUserApi.UserSimple) {
targetUser.value = user targetUser.value = user
const myNickname = userStore.userInfo?.nickname || '' const myNickname = userStore.userInfo?.nickname || ''
applyContent.value = myNickname ? `我是${myNickname}` : '' applyContent.value = myNickname ? `我是${myNickname}` : ''
@ -184,57 +175,57 @@ async function handleSubmitApply() {
<Spin :spinning="loading" wrapper-class-name="w-full"> <Spin :spinning="loading" wrapper-class-name="w-full">
<div class="h-[400px] mt-2.5"> <div class="h-[400px] mt-2.5">
<div <div
v-if="visibleUsers.length === 0" v-if="visibleUsers.length === 0"
class="py-10 text-13px text-center text-[var(--ant-color-text-disabled)]" class="py-10 text-13px text-center text-[var(--ant-color-text-disabled)]"
>
{{ searched ? '没有搜到用户' : '输入关键字后回车开始搜索' }}
</div>
<div
v-for="user in visibleUsers"
:key="user.id"
class="flex gap-3 items-center px-2 py-2.5 border-b border-b-solid border-[var(--ant-color-border-secondary)]"
>
<UserAvatar
:id="user.id"
:url="user.avatar"
:name="user.nickname"
:size="42"
:clickable="false"
/>
<div class="flex-1 min-w-0 overflow-hidden">
<!-- 昵称 + 性别图标 -->
<div
class="flex items-center gap-1 text-sm font-semibold text-[var(--ant-color-text)]"
>
<span class="truncate">{{ user.nickname }}</span>
<Icon
v-if="getGenderIcon(user.sex)"
:icon="getGenderIcon(user.sex)"
:size="14"
:color="getGenderColor(user.sex)"
class="flex-shrink-0"
/>
</div>
<!-- 部门 -->
<div
v-if="user.deptName"
class="mt-0.5 text-xs truncate text-[var(--ant-color-text-secondary)]"
>
{{ user.deptName }}
</div>
</div>
<!-- 已是好友显示已添加否则显示添加点击进入 apply 步骤 -->
<Button
v-if="!friendStore.isActiveFriend(user.id)"
type="primary"
size="small"
@click="enterApply(user)"
> >
添加 {{ searched ? '没有搜到用户' : '输入关键字后回车开始搜索' }}
</Button> </div>
<Button v-else size="small" disabled>已添加</Button> <div
</div> v-for="user in visibleUsers"
:key="user.id"
class="flex gap-3 items-center px-2 py-2.5 border-b border-b-solid border-[var(--ant-color-border-secondary)]"
>
<UserAvatar
:id="user.id"
:url="user.avatar"
:name="user.nickname"
:size="42"
:clickable="false"
/>
<div class="flex-1 min-w-0 overflow-hidden">
<!-- 昵称 + 性别图标 -->
<div
class="flex items-center gap-1 text-sm font-semibold text-[var(--ant-color-text)]"
>
<span class="truncate">{{ user.nickname }}</span>
<Icon
v-if="getGenderIcon(user.sex)"
:icon="getGenderIcon(user.sex)"
:size="14"
:color="getGenderColor(user.sex)"
class="flex-shrink-0"
/>
</div>
<!-- 部门 -->
<div
v-if="user.deptName"
class="mt-0.5 text-xs truncate text-[var(--ant-color-text-secondary)]"
>
{{ user.deptName }}
</div>
</div>
<!-- 已是好友显示已添加否则显示添加点击进入 apply 步骤 -->
<Button
v-if="!friendStore.isActiveFriend(user.id)"
type="primary"
size="small"
@click="enterApply(user)"
>
添加
</Button>
<Button v-else size="small" disabled>已添加</Button>
</div>
</div> </div>
</Spin> </Spin>
</template> </template>
@ -272,7 +263,7 @@ async function handleSubmitApply() {
:maxlength="255" :maxlength="255"
show-count show-count
placeholder="请填写申请理由" placeholder="请填写申请理由"
/> />
<div class="text-13px text-[var(--ant-color-text-secondary)] mt-3 mb-1.5">备注</div> <div class="text-13px text-[var(--ant-color-text-secondary)] mt-3 mb-1.5">备注</div>
<Input <Input

View File

@ -3,11 +3,10 @@ import type { GroupMemberLite } from './GroupMember.vue'
import { ref } from 'vue' import { ref } from 'vue'
import { Button, Modal } from 'ant-design-vue' import { Button, message, Modal } from 'ant-design-vue'
import { addGroupAdmin, removeGroupAdmin } from '#/api/im/group' import { addGroupAdmin, removeGroupAdmin } from '#/api/im/group'
import { GROUP_ADMIN_MAX_COUNT } from '#/views/im/utils/config' import { GROUP_ADMIN_MAX_COUNT } from '#/views/im/utils/config'
import { useMessage } from '#/views/im/utils/message-feedback'
import GroupMemberPickerPanel from '../picker/GroupMemberPickerPanel.vue' import GroupMemberPickerPanel from '../picker/GroupMemberPickerPanel.vue'
@ -18,14 +17,11 @@ const emit = defineEmits<{
reload: [] reload: []
}>() }>()
const message = useMessage()
const visible = ref(false) const visible = ref(false)
const submitting = ref(false) const submitting = ref(false)
const groupId = ref(0) const groupId = ref(0)
const members = ref<GroupMemberLite[]>([]) const members = ref<GroupMemberLite[]>([])
/** 当前管理员 userId 列表:默认勾选 + 提交时 diff */ const currentAdminIds = ref<number[]>([]) // userId + diff
const currentAdminIds = ref<number[]>([])
const hideIds = ref<number[]>([]) const hideIds = ref<number[]>([])
const maxSize = ref(GROUP_ADMIN_MAX_COUNT) const maxSize = ref(GROUP_ADMIN_MAX_COUNT)
const selectedIds = ref<number[]>([]) const selectedIds = ref<number[]>([])

View File

@ -27,11 +27,13 @@ const props = withDefaults(
url?: string // URL url?: string // URL
}>(), }>(),
{ {
size: 42,
radius: '15%',
clickable: false, clickable: false,
name: '',
previewable: false, previewable: false,
previewZIndex: 2000 previewZIndex: 2000,
radius: '15%',
size: 42,
url: ''
} }
) )

View File

@ -3,10 +3,9 @@ import type { FriendLite } from '../../types'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { Button, Modal } from 'ant-design-vue' import { Button, message, Modal } from 'ant-design-vue'
import { createGroup } from '#/api/im/group' import { createGroup } from '#/api/im/group'
import { useMessage } from '#/views/im/utils/message-feedback'
import { buildDefaultGroupName } from '../../../utils/group' import { buildDefaultGroupName } from '../../../utils/group'
import { useFriendStore } from '../../store/friendStore' import { useFriendStore } from '../../store/friendStore'
@ -20,7 +19,6 @@ const emit = defineEmits<{
created: [groupId: number] created: [groupId: number]
}>() }>()
const message = useMessage()
const friendStore = useFriendStore() const friendStore = useFriendStore()
const groupStore = useGroupStore() const groupStore = useGroupStore()

View File

@ -4,10 +4,13 @@ import type { GroupLite } from '../../types'
import { computed, onMounted, onUnmounted } from 'vue' import { computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { prompt } from '@vben/common-ui'
import { Input, message } from 'ant-design-vue'
import { applyJoinGroup } from '#/api/im/group/request' import { applyJoinGroup } from '#/api/im/group/request'
import { ImConversationType, ImGroupAddSource } from '../../../utils/constants' import { ImConversationType, ImGroupAddSource } from '../../../utils/constants'
import { useMessage } from '../../../utils/message-feedback'
import { getGroupDisplayName } from '../../../utils/user' import { getGroupDisplayName } from '../../../utils/user'
import { useConversationStore } from '../../store/conversationStore' import { useConversationStore } from '../../store/conversationStore'
import { useGroupStore } from '../../store/groupStore' import { useGroupStore } from '../../store/groupStore'
@ -20,7 +23,6 @@ const uiStore = useImUiStore()
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const groupStore = useGroupStore() const groupStore = useGroupStore()
const router = useRouter() const router = useRouter()
const message = useMessage()
const card = computed(() => uiStore.groupInfoCard) const card = computed(() => uiStore.groupInfoCard)
@ -65,15 +67,22 @@ function handleChat(group: GroupLite) {
/** 加入群聊:先关浮层(避免与 prompt 的 mask 互相遮挡)→ 弹申请理由(可选)→ applyJoinGroup */ /** 加入群聊:先关浮层(避免与 prompt 的 mask 互相遮挡)→ 弹申请理由(可选)→ applyJoinGroup */
async function handleApply(group: GroupLite) { async function handleApply(group: GroupLite) {
handleClose() handleClose()
let applyContent = '' let applyContent: string
try { try {
const result = await message.prompt(`申请加入「${group.name || ''}`, { const result = await prompt<string>({
cancelText: '取消', cancelText: '取消',
component: Input,
componentProps: {
allowClear: true,
placeholder: '请填写验证消息(可选)'
},
content: '',
defaultValue: '', defaultValue: '',
okText: '发送申请', confirmText: '发送申请',
placeholder: '请填写验证消息(可选)' modelPropName: 'value',
title: `申请加入「${group.name || ''}`
}) })
applyContent = (result.value || '').trim() applyContent = (result || '').trim()
} catch { } catch {
return return
} }

View File

@ -5,13 +5,12 @@ import { computed, ref } from 'vue'
import { CommonStatusEnum } from '@vben/constants' import { CommonStatusEnum } from '@vben/constants'
import { Button, Modal } from 'ant-design-vue' import { Button, message, Modal } from 'ant-design-vue'
import { inviteGroupMember } from '#/api/im/group/member' import { inviteGroupMember } from '#/api/im/group/member'
import { getCurrentUserId } from '#/views/im/utils/auth' import { getCurrentUserId } from '#/views/im/utils/auth'
import { GROUP_MAX_MEMBER } from '#/views/im/utils/config' import { GROUP_MAX_MEMBER } from '#/views/im/utils/config'
import { ImGroupMemberRole } from '#/views/im/utils/constants' import { ImGroupMemberRole } from '#/views/im/utils/constants'
import { useMessage } from '#/views/im/utils/message-feedback'
import { useFriendStore } from '../../store/friendStore' import { useFriendStore } from '../../store/friendStore'
import { useGroupStore } from '../../store/groupStore' import { useGroupStore } from '../../store/groupStore'
@ -24,7 +23,6 @@ const emit = defineEmits<{
reload: [friendIds: number[]] reload: [friendIds: number[]]
}>() }>()
const message = useMessage()
const friendStore = useFriendStore() const friendStore = useFriendStore()
const groupStore = useGroupStore() const groupStore = useGroupStore()

View File

@ -3,10 +3,9 @@ import type { GroupMemberLite } from './GroupMember.vue'
import { ref } from 'vue' import { ref } from 'vue'
import { Button, Modal } from 'ant-design-vue' import { Button, message, Modal } from 'ant-design-vue'
import { removeGroupMember } from '#/api/im/group/member' import { removeGroupMember } from '#/api/im/group/member'
import { useMessage } from '#/views/im/utils/message-feedback'
import GroupMemberPickerPanel from '../picker/GroupMemberPickerPanel.vue' import GroupMemberPickerPanel from '../picker/GroupMemberPickerPanel.vue'
@ -17,8 +16,6 @@ const emit = defineEmits<{
reload: [] reload: []
}>() }>()
const message = useMessage()
const visible = ref(false) const visible = ref(false)
const submitting = ref(false) const submitting = ref(false)
const groupId = ref(0) const groupId = ref(0)

View File

@ -1,10 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import { Button, Modal } from 'ant-design-vue' import { Button, message, Modal } from 'ant-design-vue'
import { muteMember } from '#/api/im/group' import { muteMember } from '#/api/im/group'
import { useMessage } from '#/views/im/utils/message-feedback'
defineOptions({ name: 'ImGroupMuteMemberDialog' }) defineOptions({ name: 'ImGroupMuteMemberDialog' })
@ -12,8 +11,6 @@ const emit = defineEmits<{
success: [] success: []
}>() }>()
const { success: successMessage } = useMessage()
const visible = ref(false) const visible = ref(false)
const loading = ref(false) const loading = ref(false)
const groupId = ref(0) const groupId = ref(0)
@ -49,7 +46,7 @@ async function handleConfirm() {
userId: userId.value, userId: userId.value,
mutedSeconds: selected.value mutedSeconds: selected.value
}) })
successMessage('禁言成功') message.success('禁言成功')
visible.value = false visible.value = false
emit('success') emit('success')
} finally { } finally {

View File

@ -3,10 +3,11 @@ import type { GroupMemberLite } from './GroupMember.vue'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { Button, Modal } from 'ant-design-vue' import { confirm } from '@vben/common-ui'
import { Button, message, Modal } from 'ant-design-vue'
import { transferGroupOwner } from '#/api/im/group' import { transferGroupOwner } from '#/api/im/group'
import { useMessage } from '#/views/im/utils/message-feedback'
import GroupMemberPickerPanel from '../picker/GroupMemberPickerPanel.vue' import GroupMemberPickerPanel from '../picker/GroupMemberPickerPanel.vue'
@ -17,8 +18,6 @@ const emit = defineEmits<{
reload: [] reload: []
}>() }>()
const message = useMessage()
const visible = ref(false) const visible = ref(false)
const submitting = ref(false) const submitting = ref(false)
const groupId = ref(0) const groupId = ref(0)
@ -57,7 +56,7 @@ async function handleOk() {
return return
} }
try { try {
await message.confirm( await confirm(
`确定将群主转让给 ${newOwner.value.showName}?转让后你将变为普通成员,无法撤销。`, `确定将群主转让给 ${newOwner.value.showName}?转让后你将变为普通成员,无法撤销。`,
'确认转让群主' '确认转让群主'
) )

View File

@ -1,25 +1,26 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ImGroupRequestApi } from '#/api/im/group/request'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { Empty, Modal, Spin } from 'ant-design-vue' import { prompt } from '@vben/common-ui'
import { getGroupRequestListByGroupId, type ImGroupRequestRespVO } from '#/api/im/group/request' import { Empty, Input, message, Modal, Spin } from 'ant-design-vue'
import { getGroupRequestListByGroupId } from '#/api/im/group/request'
import { ImGroupRequestHandleResult } from '#/views/im/utils/constants' import { ImGroupRequestHandleResult } from '#/views/im/utils/constants'
import { useMessage } from '#/views/im/utils/message-feedback'
import { useGroupRequestStore } from '../../store/groupRequestStore' import { useGroupRequestStore } from '../../store/groupRequestStore'
import UserAvatar from '../user/UserAvatar.vue' import UserAvatar from '../user/UserAvatar.vue'
defineOptions({ name: 'ImGroupRequestListDialog' }) defineOptions({ name: 'ImGroupRequestListDialog' })
const message = useMessage()
const groupRequestStore = useGroupRequestStore() const groupRequestStore = useGroupRequestStore()
const visible = ref(false) const visible = ref(false)
/** 当前展示的群编号undefined 时走全局未处理列表store.unhandledList */ const groupId = ref<number | undefined>() // undefined store.unhandledList
const groupId = ref<number | undefined>()
const loading = ref(false) const loading = ref(false)
const groupList = ref<ImGroupRequestRespVO[]>([]) const groupList = ref<ImGroupRequestApi.GroupRequestRespVO[]>([])
const actingId = ref<null | number>(null) const actingId = ref<null | number>(null)
defineExpose({ defineExpose({
@ -32,7 +33,7 @@ defineExpose({
}) })
/** 数据源:单群模式用 fetch 回来的 groupList全局模式直接读 store.unhandledList处理后 store 自动 reactive 同步 */ /** 数据源:单群模式用 fetch 回来的 groupList全局模式直接读 store.unhandledList处理后 store 自动 reactive 同步 */
const list = computed<ImGroupRequestRespVO[]>(() => const list = computed<ImGroupRequestApi.GroupRequestRespVO[]>(() =>
groupId.value ? groupList.value : groupRequestStore.unhandledList groupId.value ? groupList.value : groupRequestStore.unhandledList
) )
@ -104,7 +105,7 @@ async function fetchList(targetGroupId: number) {
} }
/** 同意:走 store 同步全局未处理列表 + 本地更新 handleResult 让按钮变灰 */ /** 同意:走 store 同步全局未处理列表 + 本地更新 handleResult 让按钮变灰 */
async function handleAgree(item: ImGroupRequestRespVO) { async function handleAgree(item: ImGroupRequestApi.GroupRequestRespVO) {
if (actingId.value !== null) return if (actingId.value !== null) return
actingId.value = item.id actingId.value = item.id
try { try {
@ -117,14 +118,21 @@ async function handleAgree(item: ImGroupRequestRespVO) {
} }
/** 拒绝:弹理由输入框;为空则不带 handleContent */ /** 拒绝:弹理由输入框;为空则不带 handleContent */
async function handleRefuse(item: ImGroupRequestRespVO) { async function handleRefuse(item: ImGroupRequestApi.GroupRequestRespVO) {
if (actingId.value !== null) return if (actingId.value !== null) return
let handleContent = '' let handleContent: string
try { try {
const result = await message.prompt('拒绝申请', { const result = await prompt<string>({
placeholder: '请输入拒绝理由(可选)' component: Input,
componentProps: {
allowClear: true,
placeholder: '请输入拒绝理由(可选)'
},
content: '',
modelPropName: 'value',
title: '拒绝申请'
}) })
handleContent = result.value || '' handleContent = result || ''
} catch { } catch {
return return
} }

View File

@ -5,9 +5,7 @@ import { computed, ref } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Input } from 'ant-design-vue' import { Input, message } from 'ant-design-vue'
import { useMessage } from '#/views/im/utils/message-feedback'
import { ImConversationType } from '../../../utils/constants' import { ImConversationType } from '../../../utils/constants'
import { filterConversationsByKeyword, getConversationKey } from '../../../utils/conversation' import { filterConversationsByKeyword, getConversationKey } from '../../../utils/conversation'
@ -40,17 +38,14 @@ const props = withDefaults(
) )
const emit = defineEmits<{ const emit = defineEmits<{
'create-chat': [] createChat: []
/** 用户在「最近转发」段进入移除模式后点 ×;业务壳收到后调 conversationStore.removeRecentForwardConversationKey 落盘 */ /** 用户在「最近转发」段进入移除模式后点 ×;业务壳收到后调 conversationStore.removeRecentForwardConversationKey 落盘 */
'remove-recent': [key: string] removeRecent: [key: string]
'update:selectedKeys': [value: string[]] 'update:selectedKeys': [value: string[]]
}>() }>()
const message = useMessage()
const keyword = ref('') const keyword = ref('')
/** 「最近转发」段是否处于移除模式true 时头像右上角变 × 不再切勾选 */ const recentRemoveMode = ref(false) // true ×
const recentRemoveMode = ref(false)
/** 全量会话的 key→Conversation 映射,已选 / 最近转发反查共用,避免每次 O(N) 扫 */ /** 全量会话的 key→Conversation 映射,已选 / 最近转发反查共用,避免每次 O(N) 扫 */
const byKey = computed(() => { const byKey = computed(() => {
@ -200,7 +195,7 @@ function handleToggle(conversation: Conversation) {
<span <span
v-if="recentRemoveMode" v-if="recentRemoveMode"
class="flex absolute -top-1 -right-1 justify-center items-center w-4 h-4 rounded-full cursor-pointer bg-[var(--ant-color-fill-dark)] text-[var(--ant-color-text)]" class="flex absolute -top-1 -right-1 justify-center items-center w-4 h-4 rounded-full cursor-pointer bg-[var(--ant-color-fill-dark)] text-[var(--ant-color-text)]"
@click.stop="emit('remove-recent', getConversationKey(conversation))" @click.stop="emit('removeRecent', getConversationKey(conversation))"
> >
<Icon icon="ant-design:close-outlined" :size="10" /> <Icon icon="ant-design:close-outlined" :size="10" />
</span> </span>
@ -235,7 +230,7 @@ function handleToggle(conversation: Conversation) {
<div <div
v-if="showCreateChat && !keyword.trim()" v-if="showCreateChat && !keyword.trim()"
class="flex gap-2.5 items-center px-3 py-1.5 cursor-pointer hover:bg-[var(--ant-color-fill)]" class="flex gap-2.5 items-center px-3 py-1.5 cursor-pointer hover:bg-[var(--ant-color-fill)]"
@click="emit('create-chat')" @click="emit('createChat')"
> >
<span <span
class="flex flex-shrink-0 justify-center items-center w-8 h-8 rounded-full bg-[var(--ant-color-fill)] text-[var(--ant-color-text-secondary)]" class="flex flex-shrink-0 justify-center items-center w-8 h-8 rounded-full bg-[var(--ant-color-fill)] text-[var(--ant-color-text-secondary)]"

View File

@ -5,9 +5,7 @@ import { computed, ref } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Input } from 'ant-design-vue' import { Input, message } from 'ant-design-vue'
import { useMessage } from '#/views/im/utils/message-feedback'
import { useFriendBuckets } from '../../composables/useFriendBuckets' import { useFriendBuckets } from '../../composables/useFriendBuckets'
import { useSelectedItems } from '../../composables/useSelectedItems' import { useSelectedItems } from '../../composables/useSelectedItems'
@ -43,8 +41,6 @@ const emit = defineEmits<{
'update:selectedIds': [value: number[]] 'update:selectedIds': [value: number[]]
}>() }>()
const message = useMessage()
const keyword = ref('') const keyword = ref('')
/** id → friend 映射,已选反查 / 三态判定共用,避免每次 O(N) 扫 */ /** id → friend 映射,已选反查 / 三态判定共用,避免每次 O(N) 扫 */

View File

@ -6,9 +6,7 @@ import { computed, ref } from 'vue'
import { CommonStatusEnum } from '@vben/constants' import { CommonStatusEnum } from '@vben/constants'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Input } from 'ant-design-vue' import { Input, message } from 'ant-design-vue'
import { useMessage } from '#/views/im/utils/message-feedback'
import { useSelectedItems } from '../../composables/useSelectedItems' import { useSelectedItems } from '../../composables/useSelectedItems'
import GroupMemberGrid from '../group/GroupMemberGrid.vue' import GroupMemberGrid from '../group/GroupMemberGrid.vue'
@ -48,8 +46,6 @@ const emit = defineEmits<{
'update:selectedIds': [value: number[]] 'update:selectedIds': [value: number[]]
}>() }>()
const message = useMessage()
const keyword = ref('') const keyword = ref('')
/** userId → member 映射,已选反查 / 三态判定共用 */ /** userId → member 映射,已选反查 / 三态判定共用 */

View File

@ -4,6 +4,7 @@ import type { CallParticipantVM } from './RtcCallParticipantTile.vue'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { useIntervalFn } from '@vueuse/core' import { useIntervalFn } from '@vueuse/core'
import { message } from 'ant-design-vue'
import { Track } from 'livekit-client' import { Track } from 'livekit-client'
import { import {
@ -21,7 +22,6 @@ import {
ImRtcCallMediaType, ImRtcCallMediaType,
ImRtcCallStage ImRtcCallStage
} from '#/views/im/utils/constants' } from '#/views/im/utils/constants'
import { useMessage } from '#/views/im/utils/message-feedback'
import { getSenderAvatar, getSenderDisplayName } from '#/views/im/utils/user' import { getSenderAvatar, getSenderDisplayName } from '#/views/im/utils/user'
import { useLiveKitRoom } from '../../composables/useLiveKitRoom' import { useLiveKitRoom } from '../../composables/useLiveKitRoom'
@ -34,7 +34,6 @@ import RtcCallRunning from './RtcCallRunning.vue'
defineOptions({ name: 'ImRtcCallContainer' }) defineOptions({ name: 'ImRtcCallContainer' })
const rtcStore = useRtcStore() const rtcStore = useRtcStore()
const message = useMessage()
const lk = useLiveKitRoom() const lk = useLiveKitRoom()
const memberPickerRef = ref<InstanceType<typeof RtcCallMemberPickerDialog>>() const memberPickerRef = ref<InstanceType<typeof RtcCallMemberPickerDialog>>()
@ -123,16 +122,13 @@ const participants = computed<CallParticipantVM[]>(() => {
const conversationType = call.conversationType const conversationType = call.conversationType
const targetId = call.groupId ?? 0 const targetId = call.groupId ?? 0
const myId = getCurrentUserId() const myId = getCurrentUserId()
const result: CallParticipantVM[] = [] const result: CallParticipantVM[] = [{
//
result.push({
userId: myId, userId: myId,
nickname: getSenderDisplayName(myId, conversationType, targetId), nickname: getSenderDisplayName(myId, conversationType, targetId),
avatar: getSenderAvatar(myId, conversationType, targetId) || undefined, avatar: getSenderAvatar(myId, conversationType, targetId) || undefined,
isLocal: true, isLocal: true,
videoStream: localStream.value videoStream: localStream.value
}) }]
// Camera // Camera
const joined = new Set<number>() const joined = new Set<number>()

View File

@ -31,7 +31,7 @@ const acceptDisabled = computed(() => !!props.accepting || !!props.rejecting)
/** 拒绝按钮禁用态 */ /** 拒绝按钮禁用态 */
const rejectDisabled = computed(() => !!props.rejecting || !!props.accepting) const rejectDisabled = computed(() => !!props.rejecting || !!props.accepting)
/** 群通话成员;缓存为空时用 INVITE 载荷里的主叫兜底,避免空白 */ // INVITE
const callMembers = useGroupCallMembers( const callMembers = useGroupCallMembers(
computed(() => (props.isGroup ? props.payload?.groupId : undefined)), computed(() => (props.isGroup ? props.payload?.groupId : undefined)),
computed(() => props.payload?.inviterUserId) computed(() => props.payload?.inviterUserId)

View File

@ -21,16 +21,11 @@ type PickerMode = 'add' | 'invite'
const groupStore = useGroupStore() const groupStore = useGroupStore()
/** 弹窗显隐 */ const visible = ref(false) //
const visible = ref(false) const groupId = ref(0) // open
/** 当前群编号open 时由调用方传入 */ const mode = ref<PickerMode>('invite') // 弹窗用途invite=发起群通话选邀请人 / add=通话中追加成员
const groupId = ref(0) const excludeUserIds = ref<number[]>([]) // userId add
/** 弹窗用途invite=发起群通话选邀请人 / add=通话中追加成员 */ const selectedIds = ref<number[]>([]) // userId GroupMemberPickerPanel v-model
const mode = ref<PickerMode>('invite')
/** 置灰的 userId 列表add 场景把已在通话内的人禁用 */
const excludeUserIds = ref<number[]>([])
/** 当前选中的 userId 列表GroupMemberPickerPanel v-model 绑过来 */
const selectedIds = ref<number[]>([])
/** 标题;按用途切换 */ /** 标题;按用途切换 */
const title = computed(() => (mode.value === 'add' ? '添加成员' : '选择成员')) const title = computed(() => (mode.value === 'add' ? '添加成员' : '选择成员'))

View File

@ -57,8 +57,7 @@ const remoteAudioRef = useMediaStreamElement<HTMLAudioElement>(() => props.remot
/** 1v1 视频:是否有远端视频流 */ /** 1v1 视频:是否有远端视频流 */
const hasRemoteVideo = computed(() => !props.isGroup && !!props.remoteVideoStream) const hasRemoteVideo = computed(() => !props.isGroup && !!props.remoteVideoStream)
/** 通话时长;仅 1v1 语音视图需要展示,其它视图不启 tick */ const now = ref(Date.now()) // 1v1 tick
const now = ref(Date.now())
let tick = 0 let tick = 0
watch( watch(
() => props.isGroup || props.isVideo, () => props.isGroup || props.isVideo,

View File

@ -5,11 +5,10 @@ import { DICT_TYPE } from '@vben/constants'
import { getDictLabel } from '@vben/hooks' import { getDictLabel } from '@vben/hooks'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Popover } from 'ant-design-vue' import { message, Popover } from 'ant-design-vue'
import { getActiveCall, joinCall } from '#/api/im/rtc' import { getActiveCall, joinCall } from '#/api/im/rtc'
import { getCurrentUserId } from '#/views/im/utils/auth' import { getCurrentUserId } from '#/views/im/utils/auth'
import { useMessage } from '#/views/im/utils/message-feedback'
import { useGroupCallMembers } from '../../composables/useGroupCallMembers' import { useGroupCallMembers } from '../../composables/useGroupCallMembers'
import { useRtcStore } from '../../store/rtcStore' import { useRtcStore } from '../../store/rtcStore'
@ -22,7 +21,6 @@ const props = defineProps<{
}>() }>()
const rtcStore = useRtcStore() const rtcStore = useRtcStore()
const message = useMessage()
const popoverVisible = ref(false) const popoverVisible = ref(false)

View File

@ -5,11 +5,10 @@ import { computed, ref } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Button, Input, Modal } from 'ant-design-vue' import { Button, Input, message, Modal } from 'ant-design-vue'
import { createGroup } from '#/api/im/group' import { createGroup } from '#/api/im/group'
import CardBubble from '#/views/im/home/components/card/CardBubble.vue' import CardBubble from '#/views/im/home/components/card/CardBubble.vue'
import { useMessage } from '#/views/im/utils/message-feedback'
import { ImContentType, ImConversationType, isGroupConversation } from '../../../utils/constants' import { ImContentType, ImConversationType, isGroupConversation } from '../../../utils/constants'
import { getConversationKey } from '../../../utils/conversation' import { getConversationKey } from '../../../utils/conversation'
@ -26,7 +25,6 @@ import FriendPickerPanel from '../picker/FriendPickerPanel.vue'
defineOptions({ name: 'ImRecommendCardDialog' }) defineOptions({ name: 'ImRecommendCardDialog' })
const message = useMessage()
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const friendStore = useFriendStore() const friendStore = useFriendStore()
const groupStore = useGroupStore() const groupStore = useGroupStore()
@ -34,14 +32,12 @@ const { sendRaw, send } = useMessageSender()
const visible = ref(false) const visible = ref(false)
const target = ref<CardTarget | null>(null) const target = ref<CardTarget | null>(null)
/** 当前视图:默认会话选择,「创建聊天」入口切到好友选择 */ const view = ref<'contact' | 'conversation'>('conversation') //
const view = ref<'contact' | 'conversation'>('conversation')
const selectedKeys = ref<string[]>([]) const selectedKeys = ref<string[]>([])
const selectedFriendIds = ref<number[]>([]) const selectedFriendIds = ref<number[]>([])
const leaveMessage = ref('') const leaveMessage = ref('')
const sending = ref(false) const sending = ref(false)
/** 表情面板显隐:右侧 smile icon 切换 */ const emojiVisible = ref(false) // smile icon
const emojiVisible = ref(false)
defineExpose({ defineExpose({
/** 打开推荐弹窗reset → 灌参 → visible=true */ /** 打开推荐弹窗reset → 灌参 → visible=true */

View File

@ -1,21 +1,20 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { User } from '../../types' import type { User } from '../../types'
import { computed, nextTick, ref, watch } from 'vue' import { computed, h, nextTick, ref, watch } from 'vue'
import { confirm } from '@vben/common-ui'
import { DICT_TYPE } from '@vben/constants' import { DICT_TYPE } from '@vben/constants'
import { getDictLabel } from '@vben/hooks' import { getDictLabel } from '@vben/hooks'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Button, Dropdown, Input, Menu } from 'ant-design-vue' import { Button, Checkbox, Dropdown, Input, Menu, message } from 'ant-design-vue'
import { useMessage } from '#/views/im/utils/message-feedback' import { getSimpleUser, type SystemUserApi } from '#/api/system/user'
import { getSimpleUser, type UserVO } from '#/views/im/utils/system-user'
import { formatDate } from '#/views/im/utils/time' import { formatDate } from '#/views/im/utils/time'
import { ImFriendAddSource } from '../../../utils/constants' import { ImFriendAddSource } from '../../../utils/constants'
import { toUserCardTarget } from '../../../utils/message' import { toUserCardTarget } from '../../../utils/message'
import { confirmDeleteFriend } from '../../../utils/message-feedback'
import { getGenderColor, getGenderIcon } from '../../../utils/user' import { getGenderColor, getGenderIcon } from '../../../utils/user'
import { useFriendStore } from '../../store/friendStore' import { useFriendStore } from '../../store/friendStore'
import FriendAddDialog from '../friend/FriendAddDialog.vue' import FriendAddDialog from '../friend/FriendAddDialog.vue'
@ -39,9 +38,11 @@ const props = withDefaults(
user: null | User user: null | User
}>(), }>(),
{ {
relation: 'readonly', addSource: ImFriendAddSource.SEARCH,
addSourceExtra: '',
displayName: '',
previewZIndex: 2000, previewZIndex: 2000,
addSource: ImFriendAddSource.SEARCH relation: 'readonly'
} }
) )
@ -63,11 +64,9 @@ const emit = defineEmits<{
*/ */
export type UserInfoRelation = 'friend' | 'readonly' | 'self' | 'stranger' export type UserInfoRelation = 'friend' | 'readonly' | 'self' | 'stranger'
const message = useMessage()
const friendStore = useFriendStore() const friendStore = useFriendStore()
/** 起手 user + getSimpleUser 合并后的完整对象(性别 / 部门补齐用) */ const full = ref<null | User>(props.user) // 起手 user + getSimpleUser 合并后的完整对象(性别 / 部门补齐用
const full = ref<null | User>(props.user)
/** 主标题:备注优先(好友场景),其次原昵称 */ /** 主标题:备注优先(好友场景),其次原昵称 */
const headerName = computed(() => props.displayName || full.value?.nickname || '') const headerName = computed(() => props.displayName || full.value?.nickname || '')
@ -85,8 +84,7 @@ const friendInfo = computed(() =>
/** 是否已拉黑:菜单项「加入黑名单 / 移出黑名单」按这个切换 */ /** 是否已拉黑:菜单项「加入黑名单 / 移出黑名单」按这个切换 */
const isBlocked = computed(() => !!friendInfo.value?.blocked) const isBlocked = computed(() => !!friendInfo.value?.blocked)
/** 备注内联编辑editingRemark 控制输入态user 切换时由下面的 watch 复位避免脏态泄漏 */ const editingRemark = ref(false) // editingRemark user watch
const editingRemark = ref(false)
const remarkInput = ref('') const remarkInput = ref('')
const remarkInputRef = ref<null | { focus: () => void; select?: () => void }>(null) const remarkInputRef = ref<null | { focus: () => void; select?: () => void }>(null)
@ -169,11 +167,9 @@ function handleComingSoon(featureName: string) {
// ==================== / ==================== // ==================== / ====================
/** 加好友弹窗 refhandleAddFriend 调 open({ presetUser, addSource, addSourceExtra }) 触发 */ const friendAddDialogRef = ref<InstanceType<typeof FriendAddDialog>>() // refhandleAddFriend open({ presetUser, addSource, addSourceExtra })
const friendAddDialogRef = ref<InstanceType<typeof FriendAddDialog>>()
/** 推荐名片弹窗 refhandleRecommend 调用 open({ target }) 打开 */ const recommendDialogRef = ref<InstanceType<typeof RecommendCardDialog>>() // refhandleRecommend open({ target })
const recommendDialogRef = ref<InstanceType<typeof RecommendCardDialog>>()
/** 把他推荐给朋友:弹 RecommendCardDialog 选目标会话 */ /** 把他推荐给朋友:弹 RecommendCardDialog 选目标会话 */
function handleRecommend() { function handleRecommend() {
if (!props.user?.id) { if (!props.user?.id) {
@ -191,14 +187,14 @@ function handleAddFriend() {
if (!props.user?.id) { if (!props.user?.id) {
return return
} }
const presetUser: UserVO = { const presetUser: SystemUserApi.UserSimple = {
id: props.user.id, id: props.user.id,
nickname: props.user.nickname, nickname: props.user.nickname,
avatar: props.user.avatar, avatar: props.user.avatar,
sex: props.user.sex, sex: props.user.sex,
deptId: props.user.deptId, deptId: props.user.deptId,
deptName: props.user.deptName deptName: props.user.deptName
} as UserVO } as SystemUserApi.UserSimple
friendAddDialogRef.value?.open({ friendAddDialogRef.value?.open({
presetUser, presetUser,
addSource: props.addSource, addSource: props.addSource,
@ -213,7 +209,7 @@ async function handleBlock() {
} }
const target = props.user const target = props.user
try { try {
await message.confirm(`确定将「${target.nickname || ''}」加入黑名单吗?`, '加入黑名单') await confirm(`确定将「${target.nickname || ''}」加入黑名单吗?`, '加入黑名单')
} catch { } catch {
return return
} }
@ -238,7 +234,25 @@ async function handleDeleteFriend() {
const target = props.user const target = props.user
const clearConversation = ref(true) const clearConversation = ref(true)
try { try {
await confirmDeleteFriend(target.nickname || '', clearConversation) await confirm({
cancelText: '取消',
confirmText: '删除',
content: h('div', { class: 'flex flex-col gap-3 text-sm' }, [
h('div', `确定删除好友「${target.nickname || ''}」?`),
h(
Checkbox,
{
checked: clearConversation.value,
'onUpdate:checked': (value: boolean) => {
clearConversation.value = value
}
},
() => '同时清空聊天记录'
)
]),
icon: 'warning',
title: '删除联系人'
})
} catch { } catch {
return return
} }

View File

@ -2,10 +2,12 @@ import type { Conversation, Message } from '../types'
import type { AxiosProgressEvent } from '#/api/infra/file' import type { AxiosProgressEvent } from '#/api/infra/file'
import { isOpenableUrl } from '@vben/utils'
import { message } from 'ant-design-vue'
import { uploadFile } from '#/api/infra/file'
import { getCurrentUserId } from '#/views/im/utils/auth' import { getCurrentUserId } from '#/views/im/utils/auth'
import { useMessage } from '#/views/im/utils/message-feedback'
import { updateFile } from '#/views/im/utils/upload'
import { isOpenableUrl } from '#/views/im/utils/url'
import { import {
MESSAGE_FILE_MAX_MB, MESSAGE_FILE_MAX_MB,
@ -123,7 +125,7 @@ export interface UploadAndSendMediaOptions {
* *
* useMessageSender.sendRaw ack * useMessageSender.sendRaw ack
* 1. insertMessage status=SENDINGcontent blob URL_localFile File * 1. insertMessage status=SENDINGcontent blob URL_localFile File
* 2. updateFile onUploadProgress patchMessage uploadProgressUI * 2. uploadFile onUploadProgress patchMessage uploadProgressUI
* 3. url contentpatchMessage blob URL store revoke * 3. url contentpatchMessage blob URL store revoke
* 4. sendRaw(existingClientMessageId) * 4. sendRaw(existingClientMessageId)
* *
@ -168,7 +170,6 @@ export const useMediaUploader = () => {
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const messageStore = useMessageStore() const messageStore = useMessageStore()
const muteOverlay = useMuteOverlay() const muteOverlay = useMuteOverlay()
const message = useMessage()
const { sendRaw } = useMessageSender() const { sendRaw } = useMessageSender()
/** /**
@ -361,13 +362,10 @@ export const useMediaUploader = () => {
// 2. 上传:进度回调 patch uploadProgress失败保留 _localFile 供重试 // 2. 上传:进度回调 patch uploadProgress失败保留 _localFile 供重试
let url: string | undefined let url: string | undefined
try { try {
const form = new FormData() url = await uploadFile(
form.append('file', opts.file) { file: opts.file },
const res = (await updateFile(
form,
createUploadProgressHandler(conversation, clientMessageId) createUploadProgressHandler(conversation, clientMessageId)
)) as { data?: string } )
url = res?.data
} catch (error) { } catch (error) {
console.error(`[IM] ${handler.kind}上传失败`, error) console.error(`[IM] ${handler.kind}上传失败`, error)
} }

View File

@ -1,20 +1,14 @@
import type { Message } from '../types' import type { Message } from '../types'
import type { ImChannelMessageApi } from '#/api/im/message/channel'
import type { ImGroupMessageApi } from '#/api/im/message/group'
import type { ImPrivateMessageApi } from '#/api/im/message/private'
import { watch } from 'vue' import { watch } from 'vue'
import { import { pullChannelMessages as apiPullChannelMessages } from '#/api/im/message/channel'
pullChannelMessages as apiPullChannelMessages, import { pullGroupMessages as apiPullGroupMessages } from '#/api/im/message/group'
type ImChannelMessageRespVO import { getPrivateMaxReadMessageId as apiGetPrivateMaxReadMessageId, pullPrivateMessages as apiPullPrivateMessages } from '#/api/im/message/private'
} from '#/api/im/message/channel'
import {
pullGroupMessages as apiPullGroupMessages,
type ImGroupMessageRespVO
} from '#/api/im/message/group'
import {
getPrivateMaxReadMessageId as apiGetPrivateMaxReadMessageId,
pullPrivateMessages as apiPullPrivateMessages,
type ImPrivateMessageRespVO
} from '#/api/im/message/private'
import { getCurrentUserId } from '#/views/im/utils/auth' import { getCurrentUserId } from '#/views/im/utils/auth'
import { buildChannelConversationStub } from '../../utils/channel' import { buildChannelConversationStub } from '../../utils/channel'
@ -41,7 +35,7 @@ import { type PulledMessage, useMessageStore } from '../store/messageStore'
import { useImWebSocketStore } from '../store/websocketStore' import { useImWebSocketStore } from '../store/websocketStore'
/** 三类消息 pull 接口返回的原始 VO 联合类型runMinIdPull 只需 id 推进游标,具体分发在 applyPage 内按类型 cast */ /** 三类消息 pull 接口返回的原始 VO 联合类型runMinIdPull 只需 id 推进游标,具体分发在 applyPage 内按类型 cast */
type PulledRawMessage = ImChannelMessageRespVO | ImGroupMessageRespVO | ImPrivateMessageRespVO type PulledRawMessage = ImChannelMessageApi.ChannelMessageRespVO | ImGroupMessageApi.GroupMessageRespVO | ImPrivateMessageApi.PrivateMessageRespVO
/** /**
* 线 * 线
@ -74,11 +68,11 @@ export const useMessagePuller = () => {
} }
/** 私聊会话归属:自己发的算"发给 receiverId 的会话",否则算"发送方的会话"curry currentUserId 进闭包减少 3 处调用方的样板 */ /** 私聊会话归属:自己发的算"发给 receiverId 的会话",否则算"发送方的会话"curry currentUserId 进闭包减少 3 处调用方的样板 */
const getPrivatePeerId = (message: ImPrivateMessageRespVO) => const getPrivatePeerId = (message: ImPrivateMessageApi.PrivateMessageRespVO) =>
getPrivateMessagePeerId(message, currentUserId) getPrivateMessagePeerId(message, currentUserId)
/** 服务端私聊消息 -> 本地 MessagetargetId 是会话主键(对端 userId */ /** 服务端私聊消息 -> 本地 MessagetargetId 是会话主键(对端 userId */
const convertPrivateMessage = (message: ImPrivateMessageRespVO): Message => { const convertPrivateMessage = (message: ImPrivateMessageApi.PrivateMessageRespVO): Message => {
return { return {
id: message.id, id: message.id,
clientMessageId: message.clientMessageId || generateClientMessageId(), clientMessageId: message.clientMessageId || generateClientMessageId(),
@ -94,7 +88,7 @@ export const useMessagePuller = () => {
} }
/** 服务端群聊消息 -> 本地 Message */ /** 服务端群聊消息 -> 本地 Message */
const convertGroupMessage = (message: ImGroupMessageRespVO): Message => { const convertGroupMessage = (message: ImGroupMessageApi.GroupMessageRespVO): Message => {
return { return {
id: message.id, id: message.id,
clientMessageId: message.clientMessageId || generateClientMessageId(), clientMessageId: message.clientMessageId || generateClientMessageId(),
@ -113,7 +107,7 @@ export const useMessagePuller = () => {
} }
/** 服务端频道消息 -> 本地 Message */ /** 服务端频道消息 -> 本地 Message */
const convertChannelMessage = (message: ImChannelMessageRespVO): Message => { const convertChannelMessage = (message: ImChannelMessageApi.ChannelMessageRespVO): Message => {
return { return {
id: message.id, id: message.id,
clientMessageId: message.clientMessageId || generateClientMessageId(), clientMessageId: message.clientMessageId || generateClientMessageId(),
@ -130,11 +124,11 @@ export const useMessagePuller = () => {
} }
/** 频道:会话归属到 channelIdname / avatar 暂用占位,将来接入 channelStore 后再填真值 */ /** 频道:会话归属到 channelIdname / avatar 暂用占位,将来接入 channelStore 后再填真值 */
const convertChannelConversation = (message: ImChannelMessageRespVO) => const convertChannelConversation = (message: ImChannelMessageApi.ChannelMessageRespVO) =>
buildChannelConversationStub(message.channelId) buildChannelConversationStub(message.channelId)
/** 私聊:会话归属到对端 userId */ /** 私聊:会话归属到对端 userId */
const convertPrivateConversation = (message: ImPrivateMessageRespVO) => { const convertPrivateConversation = (message: ImPrivateMessageApi.PrivateMessageRespVO) => {
const targetId = getPrivatePeerId(message) const targetId = getPrivatePeerId(message)
const friend = friendStore.getFriend(targetId) const friend = friendStore.getFriend(targetId)
return { return {
@ -147,7 +141,7 @@ export const useMessagePuller = () => {
} }
/** 群聊:会话归属到 groupId */ /** 群聊:会话归属到 groupId */
const convertGroupConversation = (message: ImGroupMessageRespVO) => { const convertGroupConversation = (message: ImGroupMessageApi.GroupMessageRespVO) => {
const group = groupStore.getGroup(message.groupId) const group = groupStore.getGroup(message.groupId)
return { return {
type: ImConversationType.GROUP, type: ImConversationType.GROUP,
@ -198,7 +192,7 @@ export const useMessagePuller = () => {
// 后端按 id 升序返回,且信号 id 一定 > 原消息 id先更新 status 再插信号所以原消息一定先到、recallMessage 找得到 // 后端按 id 升序返回,且信号 id 一定 > 原消息 id先更新 status 再插信号所以原消息一定先到、recallMessage 找得到
for (const raw of list) { for (const raw of list) {
if (isChannel) { if (isChannel) {
const message = raw as ImChannelMessageRespVO const message = raw as ImChannelMessageApi.ChannelMessageRespVO
pulledMessages.push({ pulledMessages.push({
kind: 'insert', kind: 'insert',
conversationInfo: convertChannelConversation(message), conversationInfo: convertChannelConversation(message),
@ -207,7 +201,7 @@ export const useMessagePuller = () => {
continue continue
} }
if (isPrivate) { if (isPrivate) {
const message = raw as ImPrivateMessageRespVO const message = raw as ImPrivateMessageApi.PrivateMessageRespVO
// 特殊:撤回消息的处理 // 特殊:撤回消息的处理
if (message.type === ImContentType.RECALL) { if (message.type === ImContentType.RECALL) {
pulledMessages.push({ pulledMessages.push({
@ -230,7 +224,7 @@ export const useMessagePuller = () => {
message: convertPrivateMessage(message) message: convertPrivateMessage(message)
}) })
} else { } else {
const message = raw as ImGroupMessageRespVO const message = raw as ImGroupMessageApi.GroupMessageRespVO
// 特殊:撤回消息的处理 // 特殊:撤回消息的处理
if (message.type === ImContentType.RECALL) { if (message.type === ImContentType.RECALL) {
pulledMessages.push({ pulledMessages.push({

View File

@ -3,13 +3,13 @@ import type { FriendRequest, User } from '../../types'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { prompt } from '@vben/common-ui'
import { DICT_TYPE } from '@vben/constants' import { DICT_TYPE } from '@vben/constants'
import { getDictLabel } from '@vben/hooks' import { getDictLabel } from '@vben/hooks'
import { Button } from 'ant-design-vue' import { Button, Input, message } from 'ant-design-vue'
import { getCurrentUserId } from '#/views/im/utils/auth' import { getCurrentUserId } from '#/views/im/utils/auth'
import { useMessage } from '#/views/im/utils/message-feedback'
import { ImFriendRequestHandleResult } from '../../../utils/constants' import { ImFriendRequestHandleResult } from '../../../utils/constants'
import UserAvatar from '../../components/user/UserAvatar.vue' import UserAvatar from '../../components/user/UserAvatar.vue'
@ -27,7 +27,6 @@ const emit = defineEmits<{
}>() }>()
const friendStore = useFriendStore() const friendStore = useFriendStore()
const message = useMessage()
/** 当前登录用户编号;用 computed 包一层,切账号后随 wsCache 重取,避免顶层求值在 keep-alive 实例里持有旧 id */ /** 当前登录用户编号;用 computed 包一层,切账号后随 wsCache 重取,避免顶层求值在 keep-alive 实例里持有旧 id */
const currentUserId = computed(() => getCurrentUserId()) const currentUserId = computed(() => getCurrentUserId())
@ -100,15 +99,31 @@ async function handleRefuse() {
// 1. prompt 255 reject // 1. prompt 255 reject
let handleContent: string | undefined let handleContent: string | undefined
try { try {
const result = await message.prompt('拒绝好友申请', { const result = await prompt<string>({
beforeClose(scope) {
if (!scope.isConfirm) {
return
}
if ((scope.value || '').length > 255) {
message.error('最多 255 个字符')
return false
}
},
cancelText: '取消', cancelText: '取消',
component: Input.TextArea,
componentProps: {
allowClear: true,
maxlength: 255,
placeholder: '不填则不告知对方原因',
rows: 3
},
content: '',
defaultValue: '', defaultValue: '',
okText: '拒绝', confirmText: '拒绝',
placeholder: '不填则不告知对方原因', modelPropName: 'value',
textarea: true, title: '拒绝好友申请'
validator: (value: string) => (value || '').length <= 255 || '最多 255 个字符'
}) })
handleContent = result.value || undefined handleContent = result || undefined
} catch { } catch {
return return
} }

View File

@ -47,8 +47,7 @@ const enrichedRequests = computed(() =>
props.requests.map((request) => ({ request, peer: getPeer(request) })) props.requests.map((request) => ({ request, peer: getPeer(request) }))
) )
/** 点击「加载更多」拉下一页store 内部按 maxId 游标分页 + pending 去重 */ const loadingMore = ref(false) // store maxId + pending
const loadingMore = ref(false)
async function handleLoadMore() { async function handleLoadMore() {
if (loadingMore.value) { if (loadingMore.value) {
return return

View File

@ -4,11 +4,10 @@ import type { FriendLite, FriendRequest, Group, GroupLite, User } from '../../ty
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { confirm } from '@vben/common-ui'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Input } from 'ant-design-vue' import { Input, message } from 'ant-design-vue'
import { useMessage } from '#/views/im/utils/message-feedback'
import { ImConversationType } from '../../../utils/constants' import { ImConversationType } from '../../../utils/constants'
import { StorageKeys } from '../../../utils/db' import { StorageKeys } from '../../../utils/db'
@ -30,7 +29,6 @@ const router = useRouter()
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const friendStore = useFriendStore() const friendStore = useFriendStore()
const groupStore = useGroupStore() const groupStore = useGroupStore()
const message = useMessage()
/** 用 type 判别选中是好友 / 群聊 / 好友申请 */ /** 用 type 判别选中是好友 / 群聊 / 好友申请 */
type Selection = type Selection =
@ -204,7 +202,7 @@ function handleChatGroup(group: GroupLite) {
/** 删除好友:二次确认 → store 落库 → 清空当前选中 */ /** 删除好友:二次确认 → store 落库 → 清空当前选中 */
async function handleDeleteFriend(friend: FriendLite) { async function handleDeleteFriend(friend: FriendLite) {
try { try {
await message.confirm(`确定删除好友「${friend.nickname}」吗?`, '删除联系人') await confirm(`确定删除好友「${friend.nickname}」吗?`, '删除联系人')
// friendStore.deleteFriend // friendStore.deleteFriend
await friendStore.deleteFriend(friend.id) await friendStore.deleteFriend(friend.id)
if (selection.value?.type === 'friend' && selection.value.friend.id === friend.id) { if (selection.value?.type === 'friend' && selection.value.friend.id === friend.id) {

View File

@ -4,10 +4,11 @@ import type { Conversation, GroupLite } from '../../../../types'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { confirm } from '@vben/common-ui'
import { CommonStatusEnum } from '@vben/constants' import { CommonStatusEnum } from '@vben/constants'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Button, Drawer, Input, Popover, Switch } from 'ant-design-vue' import { Button, Drawer, Input, message, Popover, Switch } from 'ant-design-vue'
import { import {
dissolveGroup, dissolveGroup,
@ -18,7 +19,6 @@ import { quitGroup, updateGroupMember } from '#/api/im/group/member'
import { getCurrentUserId } from '#/views/im/utils/auth' import { getCurrentUserId } from '#/views/im/utils/auth'
import { ImConversationType, ImGroupMemberRole } from '#/views/im/utils/constants' import { ImConversationType, ImGroupMemberRole } from '#/views/im/utils/constants'
import { toGroupCardTarget } from '#/views/im/utils/message' import { toGroupCardTarget } from '#/views/im/utils/message'
import { useMessage } from '#/views/im/utils/message-feedback'
import { isGroupQuit } from '#/views/im/utils/user' import { isGroupQuit } from '#/views/im/utils/user'
import GroupAdminSetDialog from '../../../../components/group/GroupAdminSetDialog.vue' import GroupAdminSetDialog from '../../../../components/group/GroupAdminSetDialog.vue'
@ -59,7 +59,6 @@ const emit = defineEmits<{
const MEMBER_PREVIEW_COUNT = 14 const MEMBER_PREVIEW_COUNT = 14
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const groupStore = useGroupStore() const groupStore = useGroupStore()
const message = useMessage()
const visible = computed({ const visible = computed({
get: () => props.modelValue, get: () => props.modelValue,
@ -71,8 +70,7 @@ const visible = computed({
const searchText = ref('') const searchText = ref('')
const showAllMembers = ref(false) const showAllMembers = ref(false)
/** 邀请好友入群弹窗 refhandleOpenInvite 调 open({ groupId }) 打开 */ const inviteDialogRef = ref<InstanceType<typeof GroupMemberAddDialog>>() // refhandleOpenInvite open({ groupId })
const inviteDialogRef = ref<InstanceType<typeof GroupMemberAddDialog>>()
/** 打开邀请好友入群弹窗 */ /** 打开邀请好友入群弹窗 */
function handleOpenInvite() { function handleOpenInvite() {
if (!props.group?.id) { if (!props.group?.id) {
@ -99,7 +97,7 @@ const isOwnerOrAdmin = computed(
(myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN) (myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN)
) )
// 退 + userId /** 排除已退群成员 + 关键字过滤;按角色排序:群主→管理员→普通成员(同角色按 userId 稳定) */
const visibleMembers = computed(() => { const visibleMembers = computed(() => {
return props.members return props.members
.filter( .filter(
@ -114,7 +112,7 @@ const visibleMembers = computed(() => {
}) })
}) })
// / N /** 折叠规则:搜索 / 已展开 时不折叠,其余只取前 N 个 */
const moreMembersHidden = computed( const moreMembersHidden = computed(
() => () =>
!searchText.value && !showAllMembers.value && visibleMembers.value.length > MEMBER_PREVIEW_COUNT !searchText.value && !showAllMembers.value && visibleMembers.value.length > MEMBER_PREVIEW_COUNT
@ -284,8 +282,7 @@ async function onMuteAllChange(value: boolean | number | string) {
// ==================== ==================== // ==================== ====================
/** 进群申请列表弹窗 refhandleOpenRequestList 调 open({ groupId }) 触发 */ const requestListDialogRef = ref<InstanceType<typeof GroupRequestListDialog>>() // refhandleOpenRequestList open({ groupId })
const requestListDialogRef = ref<InstanceType<typeof GroupRequestListDialog>>()
/** 打开当前群的进群申请列表 */ /** 打开当前群的进群申请列表 */
function handleOpenRequestList() { function handleOpenRequestList() {
@ -297,8 +294,7 @@ function handleOpenRequestList() {
// ==================== ==================== // ==================== ====================
/** 分享群名片弹窗 refhandleShareGroupCard 调用 open({ target }) 打开 */ const recommendCardDialogRef = ref<InstanceType<typeof RecommendCardDialog>>() // refhandleShareGroupCard open({ target })
const recommendCardDialogRef = ref<InstanceType<typeof RecommendCardDialog>>()
/** 分享群名片:把当前群作为名片消息推荐给其他会话 */ /** 分享群名片:把当前群作为名片消息推荐给其他会话 */
function handleShareGroupCard() { function handleShareGroupCard() {
@ -316,9 +312,9 @@ async function handleQuit() {
if (!props.group) { if (!props.group) {
return return
} }
// message.confirm reject return //
try { try {
await message.confirm('退出群聊后将不再接收群里的消息,确认退出吗?', '确认退出') await confirm('退出群聊后将不再接收群里的消息,确认退出吗?', '确认退出')
} catch { } catch {
return return
} }
@ -340,7 +336,7 @@ async function handleDissolve() {
return return
} }
try { try {
await message.confirm('解散后所有成员将被移出,且无法恢复,确认解散吗?', '确认解散') await confirm('解散后所有成员将被移出,且无法恢复,确认解散吗?', '确认解散')
} catch { } catch {
return return
} }
@ -355,12 +351,9 @@ async function handleDissolve() {
// ==================== ==================== // ==================== ====================
// / + + // / + +
/** 移除群成员弹窗 ref */ const removeDialogRef = ref<InstanceType<typeof GroupMemberRemoveDialog>>() // ref
const removeDialogRef = ref<InstanceType<typeof GroupMemberRemoveDialog>>() const adminSetDialogRef = ref<InstanceType<typeof GroupAdminSetDialog>>() // ref
/** 设置群管理员弹窗 ref */ const ownerTransferDialogRef = ref<InstanceType<typeof GroupOwnerTransferDialog>>() // ref
const adminSetDialogRef = ref<InstanceType<typeof GroupAdminSetDialog>>()
/** 转让群主弹窗 ref */
const ownerTransferDialogRef = ref<InstanceType<typeof GroupOwnerTransferDialog>>()
// ---------- ---------- // ---------- ----------
@ -434,7 +427,7 @@ function handleOpenTransferOwner() {
> >
<div v-if="group" class="flex flex-col h-full bg-[var(--ant-color-bg-container)]"> <div v-if="group" class="flex flex-col h-full bg-[var(--ant-color-bg-container)]">
<!-- 上部可滚动内容区 --> <!-- 上部可滚动内容区 -->
<div class="flex-1 overflow-y-auto bg-[var(--ant-color-fill-secondary)]"> <div class="flex-1 overflow-y-auto bg-[var(--im-conversation-side-bg)]">
<!-- ==================== 群成员区 ==================== --> <!-- ==================== 群成员区 ==================== -->
<div class="px-4 pt-4 pb-[10px] bg-[var(--ant-color-bg-container)]"> <div class="px-4 pt-4 pb-[10px] bg-[var(--ant-color-bg-container)]">
<Input v-model:value="searchText" placeholder="搜索群成员" allow-clear> <Input v-model:value="searchText" placeholder="搜索群成员" allow-clear>
@ -772,7 +765,7 @@ function handleOpenTransferOwner() {
<!-- ==================== 底部退出 / 解散群聊历史退群群隐藏已退群无需再退 ==================== --> <!-- ==================== 底部退出 / 解散群聊历史退群群隐藏已退群无需再退 ==================== -->
<div <div
v-if="!isQuitGroup" v-if="!isQuitGroup"
class="flex-shrink-0 px-4 pt-[14px] pb-[18px] bg-[var(--ant-color-bg-container)] border-t border-t-solid border-[var(--ant-color-border-secondary)]" class="flex-shrink-0 px-4 pt-[14px] pb-[18px] bg-[var(--ant-color-bg-container)] border-t border-t-solid border-[var(--im-border-color-lighter)]"
> >
<!-- 群主解散群聊 --> <!-- 群主解散群聊 -->
<Button <Button
@ -820,6 +813,10 @@ function handleOpenTransferOwner() {
background-color: var(--ant-color-primary-bg); background-color: var(--ant-color-primary-bg);
} }
.im-conversation-group-side__modal {
--im-conversation-side-bg: #f5f7fa;
}
/* :deep 穿透 Icon 内部 svg el-icon 全局 color 在暗色模式下被主题盖过,锁 fill 到当前色 */ /* :deep 穿透 Icon 内部 svg el-icon 全局 color 在暗色模式下被主题盖过,锁 fill 到当前色 */
.im-conversation-group-side__icon-tile :deep(svg) { .im-conversation-group-side__icon-tile :deep(svg) {
fill: currentColor !important; fill: currentColor !important;
@ -827,14 +824,17 @@ function handleOpenTransferOwner() {
/* 相邻信息行加分隔线; 相邻兄弟选择器无法用工具类表达 */ /* 相邻信息行加分隔线; 相邻兄弟选择器无法用工具类表达 */
.im-conversation-group-side__row + .im-conversation-group-side__row { .im-conversation-group-side__row + .im-conversation-group-side__row {
border-top: 1px solid var(--ant-color-border-secondary); border-top: 1px solid var(--im-border-color-lighter);
}
:global(.dark) .im-conversation-group-side__modal {
--im-conversation-side-bg: rgb(255 255 255 / 5%);
} }
</style> </style>
<!-- el-drawer append-to-body 后被传送出当前 scoped 边界scoped CSS data-v 不会落到 body <!-- AntD Drawer 被传送出当前 scoped 边界这里靠 root-class-name 压掉默认 padding -->
这里靠 modal-class .el-overlay .el-drawer__body 的祖先写一段全局规则压掉默认 padding -->
<style> <style>
.im-conversation-group-side__modal .el-drawer__body { .im-conversation-group-side__modal .ant-drawer-body {
padding: 0; padding: 0;
} }
</style> </style>

View File

@ -3,12 +3,12 @@ import type { Conversation } from '../../../../types'
import { computed } from 'vue' import { computed } from 'vue'
import { confirm } from '@vben/common-ui'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Tag } from 'ant-design-vue' import { Tag } from 'ant-design-vue'
import { buildRecallTip } from '#/views/im/utils/conversation' import { buildRecallTip } from '#/views/im/utils/conversation'
import { useMessage } from '#/views/im/utils/message-feedback'
import { formatConversationTime } from '#/views/im/utils/time' import { formatConversationTime } from '#/views/im/utils/time'
import { getSenderDisplayName } from '#/views/im/utils/user' import { getSenderDisplayName } from '#/views/im/utils/user'
@ -23,8 +23,7 @@ import { useImUiStore } from '../../../../store/uiStore'
defineOptions({ name: 'ImConversationItem' }) defineOptions({ name: 'ImConversationItem' })
/** 周中文名dayjs 的 day() 返回 0-60=周日);项目没全局装 dayjs/locale/zh-cn本地映射避免引副作用 */ // dayjs day() 0-60= dayjs/locale/zh-cn
const props = defineProps<{ const props = defineProps<{
conversation: Conversation conversation: Conversation
}>() }>()
@ -34,7 +33,6 @@ const friendStore = useFriendStore()
const groupStore = useGroupStore() const groupStore = useGroupStore()
const groupRequestStore = useGroupRequestStore() const groupRequestStore = useGroupRequestStore()
const uiStore = useImUiStore() const uiStore = useImUiStore()
const message = useMessage()
const isActive = computed( const isActive = computed(
() => () =>
@ -159,7 +157,7 @@ function handleMuted() {
/** 删除会话:二次确认后软删 */ /** 删除会话:二次确认后软删 */
async function handleDelete() { async function handleDelete() {
try { try {
await message.confirm(`确定删除与「${props.conversation.name}」的会话吗?`, '删除会话') await confirm(`确定删除与「${props.conversation.name}」的会话吗?`, '删除会话')
conversationStore.removeConversation(props.conversation.type, props.conversation.targetId) conversationStore.removeConversation(props.conversation.type, props.conversation.targetId)
} catch {} } catch {}
} }

View File

@ -5,13 +5,12 @@ import { computed, ref, watch } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Button, Drawer, Input, Popover, Spin, Switch } from 'ant-design-vue' import { Button, Drawer, Input, message, Popover, Spin, Switch } from 'ant-design-vue'
import { useConversationStore } from '#/views/im/home/store/conversationStore' import { useConversationStore } from '#/views/im/home/store/conversationStore'
import { useFriendStore } from '#/views/im/home/store/friendStore' import { useFriendStore } from '#/views/im/home/store/friendStore'
import { useGroupStore } from '#/views/im/home/store/groupStore' import { useGroupStore } from '#/views/im/home/store/groupStore'
import { ImConversationType } from '#/views/im/utils/constants' import { ImConversationType } from '#/views/im/utils/constants'
import { useMessage } from '#/views/im/utils/message-feedback'
import { getFriendDisplayName } from '#/views/im/utils/user' import { getFriendDisplayName } from '#/views/im/utils/user'
import GroupCreateDialog from '../../../../components/group/GroupCreateDialog.vue' import GroupCreateDialog from '../../../../components/group/GroupCreateDialog.vue'
@ -26,12 +25,14 @@ const props = withDefaults(
modelValue?: boolean // v-model modelValue?: boolean // v-model
}>(), }>(),
{ {
conversation: null,
friend: undefined,
modelValue: false modelValue: false
} }
) )
const emit = defineEmits<{ const emit = defineEmits<{
'open-history': [] // "" MessageHistory openHistory: [] // "" MessageHistory
'update:modelValue': [value: boolean] 'update:modelValue': [value: boolean]
}>() }>()
@ -43,13 +44,11 @@ const visible = computed({
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const friendStore = useFriendStore() const friendStore = useFriendStore()
const groupStore = useGroupStore() const groupStore = useGroupStore()
const message = useMessage()
/** tile 标签 / 后续聊天界面用的展示名:备注优先 */ /** tile 标签 / 后续聊天界面用的展示名:备注优先 */
const displayName = computed(() => (props.friend ? getFriendDisplayName(props.friend) : '')) const displayName = computed(() => (props.friend ? getFriendDisplayName(props.friend) : ''))
/** 发起群聊弹窗 refhandleOpenCreateGroup 调 open({ lockedIds }) 锁定对方 */ const createGroupDialogRef = ref<InstanceType<typeof GroupCreateDialog>>() // refhandleOpenCreateGroup open({ lockedIds })
const createGroupDialogRef = ref<InstanceType<typeof GroupCreateDialog>>()
/** 打开发起群聊弹窗:把对方默认勾上且不可取消,对应微信"基于私聊发起群聊" */ /** 打开发起群聊弹窗:把对方默认勾上且不可取消,对应微信"基于私聊发起群聊" */
function handleOpenCreateGroup() { function handleOpenCreateGroup() {
@ -227,7 +226,7 @@ function handleGroupCreated(groupId: number) {
<div class="bg-[var(--ant-color-bg-container)]"> <div class="bg-[var(--ant-color-bg-container)]">
<div <div
class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]" class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--ant-color-fill-tertiary)]"
@click="emit('open-history')" @click="emit('openHistory')"
> >
<span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">查找聊天内容</span> <span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">查找聊天内容</span>
<Icon <Icon

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ImFacePackUserItemVO } from '#/api/im/face/pack' import type { ImFacePackApi } from '#/api/im/face/pack'
import type { ImFaceUserItemVO } from '#/api/im/face/useritem' import type { ImFaceUserItemApi } from '#/api/im/face/useritem'
import { computed, onUnmounted, ref, useTemplateRef, watch } from 'vue' import { computed, onUnmounted, ref, useTemplateRef, watch } from 'vue'
@ -8,10 +8,10 @@ import { IconifyIcon as Icon } from '@vben/icons'
import { message, Tooltip } from 'ant-design-vue' import { message, Tooltip } from 'ant-design-vue'
import { uploadFile } from '#/api/infra/file'
import { useFaceStore } from '#/views/im/home/store/faceStore' import { useFaceStore } from '#/views/im/home/store/faceStore'
import { IM_EMOJI_LIST } from '#/views/im/utils/emoji' import { IM_EMOJI_LIST } from '#/views/im/utils/emoji'
import { probeImageSize } from '#/views/im/utils/image' import { probeImageSize } from '#/views/im/utils/image'
import { updateFile } from '#/views/im/utils/upload'
defineOptions({ name: 'ImFacePicker' }) defineOptions({ name: 'ImFacePicker' })
@ -26,9 +26,9 @@ const props = withDefaults(
const emit = defineEmits<{ const emit = defineEmits<{
/** 选中 Unicode emoji如 😀),调用方应插入到输入框走 TEXT 通道 */ /** 选中 Unicode emoji如 😀),调用方应插入到输入框走 TEXT 通道 */
'select-emoji': [emoji: string] selectEmoji: [emoji: string]
/** 选中表情贴图,调用方应走 FACE 消息发送 */ /** 选中表情贴图,调用方应走 FACE 消息发送 */
'select-face': [face: { height: number; name?: string; url: string; width: number; }] selectFace: [face: { height: number; name?: string; url: string; width: number; }]
'update:visible': [value: boolean] 'update:visible': [value: boolean]
}>() }>()
@ -40,41 +40,39 @@ const uploadInputRef = useTemplateRef<HTMLInputElement>('uploadInputRef')
const faceStore = useFaceStore() const faceStore = useFaceStore()
/** tab 标识常量pack:N 类用 packTabKey() 拼出,避免散落字符串字面量 */ // tab pack:N packTabKey()
const FACE_TAB = { const FACE_TAB = {
EMOJI: 'emoji', EMOJI: 'emoji',
MINE: 'mine' MINE: 'mine'
} as const } as const
const packTabKey = (packId: number) => `pack:${packId}` const packTabKey = (packId: number) => `pack:${packId}`
/** 当前激活的 tab */ const activeTab = ref<string>(FACE_TAB.EMOJI) // tab
const activeTab = ref<string>(FACE_TAB.EMOJI)
/** 是否完整模式(含个人 / 系统包 tab */ /** 是否完整模式(含个人 / 系统包 tab */
const isFullMode = computed(() => props.mode === 'full') const isFullMode = computed(() => props.mode === 'full')
/** 上传中标记,避免连续点击触发并发上传 */ const uploading = ref(false) //
const uploading = ref(false)
/** 选 emoji 字符:插到输入框;选完不关面板,方便用户连发多个 */ /** 选 emoji 字符:插到输入框;选完不关面板,方便用户连发多个 */
function handleSelectEmoji(emoji: string) { function handleSelectEmoji(emoji: string) {
emit('select-emoji', emoji) emit('selectEmoji', emoji)
} }
/** 选个人表情:直接发;点完关面板,对齐微信 */ /** 选个人表情:直接发;点完关面板,对齐微信 */
function handleSelectFaceUserItem(item: ImFaceUserItemVO) { function handleSelectFaceUserItem(item: ImFaceUserItemApi.FaceUserItem) {
emit('select-face', { url: item.url, width: item.width, height: item.height, name: item.name }) emit('selectFace', { url: item.url, width: item.width, height: item.height, name: item.name })
emit('update:visible', false) emit('update:visible', false)
} }
/** 选系统表情包内表情:直接发;点完关面板 */ /** 选系统表情包内表情:直接发;点完关面板 */
function handleSelectPackItem(item: ImFacePackUserItemVO) { function handleSelectPackItem(item: ImFacePackApi.FacePackUserItem) {
emit('select-face', { url: item.url, width: item.width, height: item.height, name: item.name }) emit('selectFace', { url: item.url, width: item.width, height: item.height, name: item.name })
emit('update:visible', false) emit('update:visible', false)
} }
/** 长按 / 右键删除个人表情 */ /** 长按 / 右键删除个人表情 */
async function handleDeleteUserItem(item: ImFaceUserItemVO) { async function handleDeleteUserItem(item: ImFaceUserItemApi.FaceUserItem) {
if (!confirm('确认删除该表情?')) { if (!confirm('确认删除该表情?')) {
return return
} }
@ -106,10 +104,7 @@ async function onUploadPicked(e: Event) {
} }
try { try {
const form = new FormData() const url = await uploadFile({ file })
form.append('file', file)
const uploadRes = (await updateFile(form)) as { data?: string }
const url = uploadRes?.data
if (!url) { if (!url) {
message.error('上传失败') message.error('上传失败')
return return
@ -158,7 +153,7 @@ onUnmounted(() => {
<!-- <!--
表情面板 tabemoji / 个人表情 / N 个系统表情包 表情面板 tabemoji / 个人表情 / N 个系统表情包
- 对齐微信 PC底部 tab 栏切换面板内容emoji 保持 Unicode仍由 TEXT 通道发送 - 对齐微信 PC底部 tab 栏切换面板内容emoji 保持 Unicode仍由 TEXT 通道发送
- 个人表情 / 系统表情走 FACE 内容类型通过 select-face 事件由调用方走 sendRaw 发送 - 个人表情 / 系统表情走 FACE 内容类型通过 selectFace 事件由调用方走 sendRaw 发送
- mode='emoji' 时只显示 emoji tab + 隐藏底部 tab 给留言 / 评论这类只发文本的场景用 - mode='emoji' 时只显示 emoji tab + 隐藏底部 tab 给留言 / 评论这类只发文本的场景用
- 定位由调用方决定通常浮在表情按钮上方 - 定位由调用方决定通常浮在表情按钮上方
--> -->

View File

@ -16,7 +16,7 @@ const props = withDefaults(
canAtAll?: boolean // 当前用户是否能 @ 全员(群主 / 管理员父组件按角色算好传入 canAtAll?: boolean // 当前用户是否能 @ 全员(群主 / 管理员父组件按角色算好传入
members: GroupMemberLite[] // members: GroupMemberLite[] //
// x + top / bottom bottom picker 沿 @ // x + top / bottom bottom picker 沿 @
position: { bottom?: number; top?: number; x: number; } position?: { bottom?: number; top?: number; x: number; }
searchText?: string // @ searchText?: string // @
visible: boolean // visible: boolean //
}>(), }>(),

View File

@ -6,9 +6,11 @@ import type { Conversation } from '#/views/im/home/types'
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { isOpenableUrl } from '@vben/utils'
import { Button, Dropdown, Menu, Tooltip } from 'ant-design-vue' import { Button, Dropdown, Menu, message, Tooltip } from 'ant-design-vue'
import { uploadFile } from '#/api/infra/file'
import { import {
ensureMediaSizeWithinLimit, ensureMediaSizeWithinLimit,
useMediaUploader useMediaUploader
@ -28,9 +30,6 @@ import {
serializeMessage, serializeMessage,
withQuotePayload withQuotePayload
} from '#/views/im/utils/message' } from '#/views/im/utils/message'
import { useMessage } from '#/views/im/utils/message-feedback'
import { updateFile } from '#/views/im/utils/upload'
import { isOpenableUrl } from '#/views/im/utils/url'
import { getMemberDisplayName } from '#/views/im/utils/user' import { getMemberDisplayName } from '#/views/im/utils/user'
import ReplyPreview from '../message/ReplyPreview.vue' import ReplyPreview from '../message/ReplyPreview.vue'
@ -43,7 +42,6 @@ defineOptions({ name: 'ImMessageInput' })
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const groupStore = useGroupStore() const groupStore = useGroupStore()
const friendStore = useFriendStore() const friendStore = useFriendStore()
const message = useMessage()
const { send, sendRaw } = useMessageSender() const { send, sendRaw } = useMessageSender()
const { const {
uploadAndSendMedia, uploadAndSendMedia,
@ -518,8 +516,7 @@ const mentionPosition = ref<{ bottom?: number; top?: number; x: number; }>({ x:
/** MentionPicker w-50 沿 /** MentionPicker w-50 沿
* 高度不再用常量算位置bottom 锚定后 picker 内容多寡都不影响下沿位置自然贴 @ */ * 高度不再用常量算位置bottom 锚定后 picker 内容多寡都不影响下沿位置自然贴 @ */
const MENTION_WIDTH = 200 const MENTION_WIDTH = 200
/** 上方剩余空间至少这么多才放上方,否则翻到下方(避免 picker 被视口顶 / 顶部 chat header 切掉) */ const MENTION_MIN_FIT_ABOVE = 120 // 上方剩余空间至少这么多才放上方,否则翻到下方(避免 picker 被视口顶 / 顶部 chat header 切掉
const MENTION_MIN_FIT_ABOVE = 120
/** 当前 @ 关键词在 editor 里的范围onMentionSelect 用它定位删除 + 插入 token */ /** 当前 @ 关键词在 editor 里的范围onMentionSelect 用它定位删除 + 插入 token */
let mentionRange: null | Range = null let mentionRange: null | Range = null
@ -913,17 +910,15 @@ async function uploadAndSendVideo(file: File) {
// 2. probe probe cover // 2. probe probe cover
// 2.1 async IIFE await lint floating promise // 2.1 async IIFE await lint floating promise
// url=undefined step 3 url // url=undefined step 3 url
const videoForm = new FormData() const videoUploadPromise: Promise<string | undefined> = (async () => {
videoForm.append('file', file)
const videoUploadPromise: Promise<{ data?: string }> = (async () => {
try { try {
return (await updateFile( return await uploadFile(
videoForm, { file },
createUploadProgressHandler(conversation, clientMessageId) createUploadProgressHandler(conversation, clientMessageId)
)) as { data?: string } )
} catch (error) { } catch (error) {
console.warn('[IM] 视频本体上传失败', error) console.warn('[IM] 视频本体上传失败', error)
return { data: undefined } return undefined
} }
})() })()
// 2.2 probe + blob probe // 2.2 probe + blob probe
@ -937,12 +932,9 @@ async function uploadAndSendVideo(file: File) {
return { probe, coverUrl: undefined as string | undefined } return { probe, coverUrl: undefined as string | undefined }
} }
try { try {
const coverForm = new FormData() const coverUrl = await uploadFile({
coverForm.append( file: new File([probe.cover], `cover-${Date.now()}.jpg`, { type: 'image/jpeg' })
'file', })
new File([probe.cover], `cover-${Date.now()}.jpg`, { type: 'image/jpeg' })
)
const coverUrl = ((await updateFile(coverForm)) as { data?: string })?.data || undefined
return { probe, coverUrl } return { probe, coverUrl }
} catch (error) { } catch (error) {
console.warn('[IM] 视频封面上传失败', error) console.warn('[IM] 视频封面上传失败', error)
@ -957,7 +949,7 @@ async function uploadAndSendVideo(file: File) {
coverUploadPromise coverUploadPromise
]) ])
// 3.2 url FAILED / _localFile // 3.2 url FAILED / _localFile
const url = videoRes?.data const url = videoRes
if (!url) { if (!url) {
markMediaFailed(conversation.type, conversation.targetId, clientMessageId) markMediaFailed(conversation.type, conversation.targetId, clientMessageId)
return return
@ -1017,11 +1009,11 @@ async function onVideoPicked(e: Event) {
<!-- 禁言 / 封禁覆盖层优先级 封禁 > 全群禁言 > 成员禁言 --> <!-- 禁言 / 封禁覆盖层优先级 封禁 > 全群禁言 > 成员禁言 -->
<div <div
v-if="muteOverlay" v-if="muteOverlay"
class="absolute top-2 right-3 bottom-3 left-3 z-10 flex items-center justify-center gap-2 rounded-lg border border-solid text-sm" class="message-input__mute-overlay absolute top-2 right-3 bottom-3 left-3 z-20 flex items-center justify-center gap-2 rounded-lg border border-solid text-sm"
:class=" :class="
muteOverlay.icon === 'ant-design:stop-outlined' muteOverlay.icon === 'ant-design:stop-outlined'
? 'text-[var(--ant-color-error-active)] bg-[var(--ant-color-error-bg)] border-[var(--ant-color-error-hover)]' ? 'message-input__mute-overlay--error'
: 'text-[var(--ant-color-warning-active)] bg-[var(--ant-color-warning-bg)] border-[var(--ant-color-warning-hover)]' : 'message-input__mute-overlay--warning'
" "
> >
<Icon :icon="muteOverlay.icon" :size="18" /> <Icon :icon="muteOverlay.icon" :size="18" />
@ -1182,6 +1174,30 @@ async function onVideoPicked(e: Event) {
color: var(--ant-color-primary) !important; color: var(--ant-color-primary) !important;
} }
.message-input__mute-overlay--warning {
color: #d48806;
background: #fff7e6;
border-color: #ffd591;
}
.message-input__mute-overlay--error {
color: #cf1322;
background: #fff1f0;
border-color: #ffa39e;
}
:global(.dark) .message-input__mute-overlay--warning {
color: #ffd666;
background: rgb(77 56 21 / 88%);
border-color: #ad6800;
}
:global(.dark) .message-input__mute-overlay--error {
color: #ff7875;
background: rgb(91 33 33 / 88%);
border-color: #a8071a;
}
/* 用 data-empty 而非 :empty浏览器在删空后会留下 <br>:empty 不命中data-empty 由 syncEditorState 维护 */ /* 用 data-empty 而非 :empty浏览器在删空后会留下 <br>:empty 不命中data-empty 由 syncEditorState 维护 */
.message-input__editor[data-empty]::before { .message-input__editor[data-empty]::before {
content: attr(data-placeholder); content: attr(data-placeholder);

View File

@ -3,6 +3,7 @@ import type { Message } from '#/views/im/home/types'
import { computed, inject } from 'vue' import { computed, inject } from 'vue'
import { confirm } from '@vben/common-ui'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { useMessageMultiSelect } from '#/views/im/home/composables/useMessageMultiSelect' import { useMessageMultiSelect } from '#/views/im/home/composables/useMessageMultiSelect'
@ -10,7 +11,6 @@ import { useConversationStore } from '#/views/im/home/store/conversationStore'
import { useMessageStore } from '#/views/im/home/store/messageStore' import { useMessageStore } from '#/views/im/home/store/messageStore'
import { ImForwardMode, isNormalMessage } from '#/views/im/utils/constants' import { ImForwardMode, isNormalMessage } from '#/views/im/utils/constants'
import { getClientConversationId } from '#/views/im/utils/db' import { getClientConversationId } from '#/views/im/utils/db'
import { useMessage } from '#/views/im/utils/message-feedback'
import { IM_FORWARD_DIALOG_KEY } from '../message/forward/keys' import { IM_FORWARD_DIALOG_KEY } from '../message/forward/keys'
@ -18,7 +18,6 @@ defineOptions({ name: 'ImMessageMultiSelectBar' })
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const messageStore = useMessageStore() const messageStore = useMessageStore()
const message = useMessage()
const openForwardDialog = inject(IM_FORWARD_DIALOG_KEY) const openForwardDialog = inject(IM_FORWARD_DIALOG_KEY)
const multiSelect = useMessageMultiSelect() const multiSelect = useMessageMultiSelect()
@ -82,7 +81,12 @@ async function handleDelete() {
return return
} }
try { try {
await message.delConfirm(`确认删除选中的 ${messages.length} 条消息?`) await confirm(`确认删除选中的 ${messages.length} 条消息?`, {
cancelText: '取消',
confirmText: '确定',
icon: 'warning',
title: '删除确认'
})
} catch { } catch {
return return
} }

View File

@ -9,9 +9,8 @@ import {
watch watch
} from 'vue' } from 'vue'
import { Button } from 'ant-design-vue' import { Button, message } from 'ant-design-vue'
import { useMessage } from '#/views/im/utils/message-feedback'
import { formatSeconds } from '#/views/im/utils/time' import { formatSeconds } from '#/views/im/utils/time'
defineOptions({ name: 'ImVoiceRecorder' }) defineOptions({ name: 'ImVoiceRecorder' })
@ -31,8 +30,6 @@ const emit = defineEmits<{
'update:modelValue': [value: boolean] 'update:modelValue': [value: boolean]
}>() }>()
const message = useMessage()
const VOICE_MIME_TYPE_OPTIONS = [ const VOICE_MIME_TYPE_OPTIONS = [
{ extension: 'webm', mimeType: 'audio/webm;codecs=opus' }, { extension: 'webm', mimeType: 'audio/webm;codecs=opus' },
{ extension: 'webm', mimeType: 'audio/webm' }, { extension: 'webm', mimeType: 'audio/webm' },

View File

@ -5,13 +5,12 @@ import { computed, ref, watch } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Button } from 'ant-design-vue' import { Button, message } from 'ant-design-vue'
import { unpinGroupMessage as apiUnpinGroupMessage } from '#/api/im/group' import { unpinGroupMessage as apiUnpinGroupMessage } from '#/api/im/group'
import { getCurrentUserId } from '#/views/im/utils/auth' import { getCurrentUserId } from '#/views/im/utils/auth'
import { ImConversationType, ImGroupMemberRole } from '#/views/im/utils/constants' import { ImConversationType, ImGroupMemberRole } from '#/views/im/utils/constants'
import { resolveConversationLastContent } from '#/views/im/utils/conversation' import { resolveConversationLastContent } from '#/views/im/utils/conversation'
import { useMessage } from '#/views/im/utils/message-feedback'
import { getSenderDisplayName, isGroupQuit } from '#/views/im/utils/user' import { getSenderDisplayName, isGroupQuit } from '#/views/im/utils/user'
import { useGroupStore } from '../../../../store/groupStore' import { useGroupStore } from '../../../../store/groupStore'
@ -29,7 +28,6 @@ const emit = defineEmits<{
}>() }>()
const groupStore = useGroupStore() const groupStore = useGroupStore()
const message = useMessage()
/** 当前群(含 pinnedMessages */ /** 当前群(含 pinnedMessages */
const group = computed(() => groupStore.getGroup(props.groupId)) const group = computed(() => groupStore.getGroup(props.groupId))

View File

@ -19,8 +19,7 @@ const props = defineProps<{
const groupStore = useGroupStore() const groupStore = useGroupStore()
const groupRequestStore = useGroupRequestStore() const groupRequestStore = useGroupRequestStore()
/** 申请列表弹窗 refhandleOpen 调 open({ groupId }) 触发 */ const requestListDialogRef = ref<InstanceType<typeof GroupRequestListDialog>>() // refhandleOpen open({ groupId })
const requestListDialogRef = ref<InstanceType<typeof GroupRequestListDialog>>()
/** 打开当前群的进群申请列表 */ /** 打开当前群的进群申请列表 */
function handleOpen() { function handleOpen() {

View File

@ -2,6 +2,7 @@
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { openSafeUrl } from '@vben/utils'
import { Modal, Spin } from 'ant-design-vue' import { Modal, Spin } from 'ant-design-vue'
@ -10,7 +11,6 @@ import { useChannelStore } from '#/views/im/home/store/channelStore'
import { useConversationStore } from '#/views/im/home/store/conversationStore' import { useConversationStore } from '#/views/im/home/store/conversationStore'
import { ImConversationType } from '#/views/im/utils/constants' import { ImConversationType } from '#/views/im/utils/constants'
import { type MaterialMessage, parseMessage } from '#/views/im/utils/message' import { type MaterialMessage, parseMessage } from '#/views/im/utils/message'
import { openSafeUrl } from '#/views/im/utils/url'
const props = defineProps<{ const props = defineProps<{
content: string content: string
@ -38,7 +38,7 @@ const detailVisible = ref(false)
const detailLoading = ref(false) const detailLoading = ref(false)
const detailHtml = ref('') const detailHtml = ref('')
/** 点击行为url 非空跳外链;为空则按 payload.materialId 拉富文本正文,全屏 dialog 渲染 */ // url payload.materialId dialog
const onClick = async () => { const onClick = async () => {
if (payload.value.url) { if (payload.value.url) {
openSafeUrl(payload.value.url) openSafeUrl(payload.value.url)

View File

@ -2,7 +2,7 @@
import { computed, onBeforeUnmount } from 'vue' import { computed, onBeforeUnmount } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { formatFileSize } from '@vben/utils' import { formatFileSize, openSafeUrl } from '@vben/utils'
import { Image } from 'ant-design-vue' import { Image } from 'ant-design-vue'
@ -26,7 +26,6 @@ import {
type VideoMessage type VideoMessage
} from '#/views/im/utils/message' } from '#/views/im/utils/message'
import { formatSeconds } from '#/views/im/utils/time' import { formatSeconds } from '#/views/im/utils/time'
import { openSafeUrl } from '#/views/im/utils/url'
import MaterialBubble from './MaterialBubble.vue' import MaterialBubble from './MaterialBubble.vue'
import TipSegments from './TipSegments.vue' import TipSegments from './TipSegments.vue'
@ -48,9 +47,9 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
/** 名片点击:调用方决定弹卡片 / 跳群等行为 */ /** 名片点击:调用方决定弹卡片 / 跳群等行为 */
'click-card': [card: CardMessage, e: MouseEvent] clickCard: [card: CardMessage, e: MouseEvent]
/** 合并消息气泡点击:调用方决定开 dialog 或栈内 push */ /** 合并消息气泡点击:调用方决定开 dialog 或栈内 push */
'open-merge': [content: string] openMerge: [content: string]
}>() }>()
/** 各 type 判定 */ /** 各 type 判定 */
@ -117,8 +116,7 @@ const mergePreviewLines = computed(() => {
.map((item) => `${item.senderNickname}${summarizeMessageContent(item)}`) .map((item) => `${item.senderNickname}${summarizeMessageContent(item)}`)
}) })
/** 表情 payload非法宽高派生成 undefined让 <img> 走 CSS max-w / max-h 兜底 */ const FACE_DIMENSION_MAX = 2048 // 表情 payload非法宽高派生成 undefined让 <img> 走 CSS max-w / max-h 兜底
const FACE_DIMENSION_MAX = 2048
const facePayload = computed(() => { const facePayload = computed(() => {
if (!isFace.value) { if (!isFace.value) {
return null return null
@ -143,6 +141,7 @@ function bubbleClass(variant: 'file' | 'text' | 'voice'): string[] {
case 'file': { case 'file': {
return [ return [
side, side,
'message-bubble--file',
isSelf isSelf
? 'bg-[#95ec69] border-[var(--ant-color-border-secondary)]' ? 'bg-[#95ec69] border-[var(--ant-color-border-secondary)]'
: 'bg-[var(--ant-color-bg-container)] border-[var(--ant-color-border-secondary)] hover:border-[#409eff]' : 'bg-[var(--ant-color-bg-container)] border-[var(--ant-color-border-secondary)] hover:border-[#409eff]'
@ -151,13 +150,14 @@ function bubbleClass(variant: 'file' | 'text' | 'voice'): string[] {
case 'text': { case 'text': {
return [ return [
side, side,
'message-bubble--text',
isSelf isSelf
? 'text-black bg-[#95ec69]' ? 'text-black bg-[#95ec69]'
: 'text-[var(--ant-color-text)] bg-[var(--ant-color-fill-secondary)]' : 'text-[var(--ant-color-text)]'
] ]
} }
case 'voice': { case 'voice': {
return [side, isSelf ? 'bg-[#95ec69]' : 'bg-[var(--ant-color-fill-secondary)]'] return [side, 'message-bubble--voice', isSelf ? 'bg-[#95ec69]' : '']
} }
} }
} }
@ -170,8 +170,7 @@ function handleFileClick() {
openSafeUrl(filePayload.value.url) openSafeUrl(filePayload.value.url)
} }
/** 语音点击:托管给 useVoicePlayer 全局互斥播放,新点的语音会停掉旧的 */ const voicePlayer = useVoicePlayer() // useVoicePlayer
const voicePlayer = useVoicePlayer()
/** /**
* 实例级唯一播放 key每个 MessageBubble 实例独立一份 * 实例级唯一播放 key每个 MessageBubble 实例独立一份
* *
@ -308,14 +307,14 @@ onBeforeUnmount(() => {
v-else-if="isCard && cardPayload" v-else-if="isCard && cardPayload"
:card="cardPayload" :card="cardPayload"
clickable clickable
@click="(e: MouseEvent) => emit('click-card', cardPayload!, e)" @click="(e: MouseEvent) => emit('clickCard', cardPayload!, e)"
/> />
<!-- 合并转发气泡title + 摘要预览 + 底部聊天记录标签 --> <!-- 合并转发气泡title + 摘要预览 + 底部聊天记录标签 -->
<div <div
v-else-if="isMerge && mergePayload" v-else-if="isMerge && mergePayload"
class="flex flex-col w-[260px] rounded-md overflow-hidden cursor-pointer bg-[var(--ant-color-bg-container)] border border-solid border-[var(--ant-color-border)] hover:border-[#409eff]" class="flex flex-col w-[260px] rounded-md overflow-hidden cursor-pointer bg-[var(--ant-color-bg-container)] border border-solid border-[var(--ant-color-border)] hover:border-[#409eff]"
@click="emit('open-merge', content)" @click="emit('openMerge', content)"
> >
<div class="px-3 py-2 text-sm font-medium text-[var(--ant-color-text)] truncate"> <div class="px-3 py-2 text-sm font-medium text-[var(--ant-color-text)] truncate">
{{ mergePayload.title }} {{ mergePayload.title }}
@ -367,10 +366,17 @@ onBeforeUnmount(() => {
height: 0; height: 0;
border-style: solid; border-style: solid;
} }
.message-bubble--other {
--im-message-bubble-other-bg: #f5f5f5;
}
.message-bubble--other.message-bubble--text,
.message-bubble--other.message-bubble--voice {
background-color: var(--im-message-bubble-other-bg);
}
.message-bubble--other::before { .message-bubble--other::before {
left: -5px; left: -5px;
border-width: 5px 6px 5px 0; border-width: 5px 6px 5px 0;
border-color: transparent var(--ant-color-fill-secondary) transparent transparent; border-color: transparent var(--im-message-bubble-other-bg) transparent transparent;
} }
.message-bubble--self::before { .message-bubble--self::before {
right: -5px; right: -5px;
@ -378,6 +384,10 @@ onBeforeUnmount(() => {
border-color: transparent transparent transparent #95ec69; border-color: transparent transparent transparent #95ec69;
} }
:global(.dark) .message-bubble--other {
--im-message-bubble-other-bg: rgb(255 255 255 / 12%);
}
/* :deep 穿透 scoped 子组件 DOMel-icon 在暗色模式下全局 color 被 .el-icon{color:var(--color)} 干扰,把 voice 图标 fill 锁死 */ /* :deep 穿透 scoped 子组件 DOMel-icon 在暗色模式下全局 color 被 .el-icon{color:var(--color)} 干扰,把 voice 图标 fill 锁死 */
.message-bubble__voice-icon :deep(svg) { .message-bubble__voice-icon :deep(svg) {
fill: #606266 !important; fill: #606266 !important;

View File

@ -302,11 +302,11 @@ function matchesActiveFilter(message: Message): boolean {
*/ */
const currentList = computed<Message[]>(() => { const currentList = computed<Message[]>(() => {
const trimmedKeyword = keyword.value.trim() const trimmedKeyword = keyword.value.trim()
let list = allMessages.value.filter(matchesActiveFilter) let list = allMessages.value.filter((message) => matchesActiveFilter(message))
if (trimmedKeyword) { if (trimmedKeyword) {
list = list.filter((message) => textSnippetOf(message).includes(trimmedKeyword)) list = list.filter((message) => textSnippetOf(message).includes(trimmedKeyword))
} }
return [...list].reverse() return list.toReversed()
}) })
// ==================== ==================== // ==================== ====================
@ -343,9 +343,12 @@ async function loadEarlier() {
// maxId id // maxId id
// id // id
// / reduce POSITIVE_INFINITY undefined // / reduce POSITIVE_INFINITY undefined
const earliestId = allMessages.value let earliestId = Number.POSITIVE_INFINITY
.filter((message) => !!message.id && message.id > 0) for (const message of allMessages.value) {
.reduce((min, message) => Math.min(min, message.id || min), Number.POSITIVE_INFINITY) if (message.id && message.id > 0) {
earliestId = Math.min(earliestId, message.id)
}
}
const maxId = Number.isFinite(earliestId) ? earliestId : undefined const maxId = Number.isFinite(earliestId) ? earliestId : undefined
// list / useMessagePuller // list / useMessagePuller
@ -358,7 +361,7 @@ async function loadEarlier() {
maxId, maxId,
limit: HISTORY_PAGE_SIZE limit: HISTORY_PAGE_SIZE
}) })
earlier = (list || []).map(convertGroupMessage) earlier = (list || []).map((message) => convertGroupMessage(message))
pageLength = list?.length ?? 0 pageLength = list?.length ?? 0
} else { } else {
const list = await apiGetPrivateMessageList({ const list = await apiGetPrivateMessageList({
@ -366,7 +369,7 @@ async function loadEarlier() {
maxId, maxId,
limit: HISTORY_PAGE_SIZE limit: HISTORY_PAGE_SIZE
}) })
earlier = (list || []).map(convertPrivateMessage) earlier = (list || []).map((message) => convertPrivateMessage(message))
pageLength = list?.length ?? 0 pageLength = list?.length ?? 0
} }

View File

@ -4,11 +4,12 @@ import type { Message } from '../../../../types'
import { computed, inject } from 'vue' import { computed, inject } from 'vue'
import { confirm } from '@vben/common-ui'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { useUserStore } from '@vben/stores' import { useUserStore } from '@vben/stores'
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import { Tag } from 'ant-design-vue' import { message as antdMessage, Tag } from 'ant-design-vue'
import { pinGroupMessage as apiPinGroupMessage, cancelMuteMember } from '#/api/im/group' import { pinGroupMessage as apiPinGroupMessage, cancelMuteMember } from '#/api/im/group'
import { removeGroupMember } from '#/api/im/group/member' import { removeGroupMember } from '#/api/im/group/member'
@ -50,7 +51,6 @@ import {
resolveRtcCallPrivateBubbleText, resolveRtcCallPrivateBubbleText,
resolveRtcCallTipSegments resolveRtcCallTipSegments
} from '#/views/im/utils/message' } from '#/views/im/utils/message'
import { useMessage } from '#/views/im/utils/message-feedback'
import { formatTimeTip } from '#/views/im/utils/time' import { formatTimeTip } from '#/views/im/utils/time'
import { import {
getMemberDisplayName, getMemberDisplayName,
@ -110,8 +110,6 @@ const uiStore = useImUiStore()
const { recall, sendRaw } = useMessageSender() const { recall, sendRaw } = useMessageSender()
const { uploadAndSendMedia } = useMediaUploader() const { uploadAndSendMedia } = useMediaUploader()
const muteOverlay = useMuteOverlay() const muteOverlay = useMuteOverlay()
// confirm message props.message vue/no-dupe-keys
const { confirm: confirmDialog, success: successMessage } = useMessage()
// legacy:true HTTP navigator.clipboard execCommand // legacy:true HTTP navigator.clipboard execCommand
const { copy: copyToClipboard } = useClipboard({ legacy: true }) const { copy: copyToClipboard } = useClipboard({ legacy: true })
@ -219,8 +217,7 @@ const rtcCallTipSegments = computed(() => resolveRtcCallTipSegments(props.messag
/** 引用对象:气泡内嵌入展示;非引用消息返回 null模板 v-if 不渲染 */ /** 引用对象:气泡内嵌入展示;非引用消息返回 null模板 v-if 不渲染 */
const quote = computed(() => getQuoteFromMessage(props.message.content)) const quote = computed(() => getQuoteFromMessage(props.message.content))
/** MessagePanel 注入的弹窗触发函数 */ const openForwardDialog = inject(IM_FORWARD_DIALOG_KEY) // MessagePanel
const openForwardDialog = inject(IM_FORWARD_DIALOG_KEY)
const openMergeDetail = inject(IM_MERGE_DETAIL_DIALOG_KEY) const openMergeDetail = inject(IM_MERGE_DETAIL_DIALOG_KEY)
const redialRtcCall = inject(IM_RTC_REDIAL_KEY) const redialRtcCall = inject(IM_RTC_REDIAL_KEY)
@ -233,8 +230,7 @@ function handleRtcCallBubbleClick() {
redialRtcCall?.(mediaType) redialRtcCall?.(mediaType)
} }
/** 多选模式:模块级单例 composable */ const multiSelect = useMessageMultiSelect() // composable
const multiSelect = useMessageMultiSelect()
/** 合并消息气泡点击:打开详情弹窗(嵌套合并由弹窗内部 push 栈) */ /** 合并消息气泡点击:打开详情弹窗(嵌套合并由弹窗内部 push 栈) */
function handleMergeOpen(content: string) { function handleMergeOpen(content: string) {
@ -399,7 +395,7 @@ const isAtMe = computed(() => {
// ==================== / ==================== // ==================== / ====================
/** 右键菜单 key 常量push 端和分发端从同一处取typo 编译期就能抓 */ // key push typo
const MENU_KEYS = { const MENU_KEYS = {
COPY: 'COPY', COPY: 'COPY',
REPLY: 'REPLY', REPLY: 'REPLY',
@ -605,7 +601,7 @@ async function handleAddToFace() {
} }
const data = await faceStore.addFaceUserItem(payload) const data = await faceStore.addFaceUserItem(payload)
if (data) { if (data) {
successMessage('已添加到表情') antdMessage.success('已添加到表情')
} }
} }
@ -688,9 +684,9 @@ async function handlePin() {
return return
} }
try { try {
await confirmDialog('将在当前群成员的聊天中置顶', '置顶消息') await confirm('将在当前群成员的聊天中置顶', '置顶消息')
await apiPinGroupMessage({ id: group.id, messageId: props.message.id }) await apiPinGroupMessage({ id: group.id, messageId: props.message.id })
successMessage('已置顶') antdMessage.success('已置顶')
} catch {} } catch {}
} }
@ -701,7 +697,7 @@ async function handleCopy() {
return return
} }
await copyToClipboard(text) await copyToClipboard(text)
successMessage('内容已复制到剪贴板') antdMessage.success('内容已复制到剪贴板')
} }
/** 进入引用模式:把当前消息构造成 QuoteMessage 写入会话草稿 */ /** 进入引用模式:把当前消息构造成 QuoteMessage 写入会话草稿 */
@ -739,7 +735,7 @@ function handleEnterMultiSelect() {
*/ */
async function handleRecall() { async function handleRecall() {
try { try {
await confirmDialog('确定要撤回这条消息吗?', '撤回消息') await confirm('确定要撤回这条消息吗?', '撤回消息')
await recall(props.message) await recall(props.message)
} catch {} } catch {}
} }
@ -820,9 +816,9 @@ async function handleUnmute() {
return return
} }
try { try {
await confirmDialog('确定解除该成员的禁言吗?', '解除禁言') await confirm('确定解除该成员的禁言吗?', '解除禁言')
await cancelMuteMember({ id: group.id, userId: props.message.senderId }) await cancelMuteMember({ id: group.id, userId: props.message.senderId })
successMessage('已解除禁言') antdMessage.success('已解除禁言')
emit('reload') emit('reload')
} catch {} } catch {}
} }
@ -835,9 +831,9 @@ async function handleKick() {
} }
const name = senderDisplayName.value || '该成员' const name = senderDisplayName.value || '该成员'
try { try {
await confirmDialog(`确定将「${name}」移出群聊吗?`, '移除成员') await confirm(`确定将「${name}」移出群聊吗?`, '移除成员')
await removeGroupMember({ groupId: group.id, memberUserIds: [props.message.senderId] }) await removeGroupMember({ groupId: group.id, memberUserIds: [props.message.senderId] })
successMessage('已移除') antdMessage.success('已移除')
emit('reload') emit('reload')
} catch {} } catch {}
} }

View File

@ -6,13 +6,12 @@ import { computed, nextTick, provide, ref, watch } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Popover, Tooltip } from 'ant-design-vue' import { message, Popover, Tooltip } from 'ant-design-vue'
import { createCall } from '#/api/im/rtc' import { createCall } from '#/api/im/rtc'
import { ImConversationType, ImRtcCallMediaType, ImRtcCallStatus } from '#/views/im/utils/constants' import { ImConversationType, ImRtcCallMediaType, ImRtcCallStatus } from '#/views/im/utils/constants'
import { getClientConversationId } from '#/views/im/utils/db' import { getClientConversationId } from '#/views/im/utils/db'
import { resolveCallEndReasonText } from '#/views/im/utils/message' import { resolveCallEndReasonText } from '#/views/im/utils/message'
import { useMessage } from '#/views/im/utils/message-feedback'
import { getMemberDisplayName, isGroupQuit } from '#/views/im/utils/user' import { getMemberDisplayName, isGroupQuit } from '#/views/im/utils/user'
import GroupMuteMemberDialog from '../../../../components/group/GroupMuteMemberDialog.vue' import GroupMuteMemberDialog from '../../../../components/group/GroupMuteMemberDialog.vue'
@ -49,7 +48,6 @@ const messageStore = useMessageStore()
const friendStore = useFriendStore() const friendStore = useFriendStore()
const uiStore = useImUiStore() const uiStore = useImUiStore()
const groupStore = useGroupStore() const groupStore = useGroupStore()
const message = useMessage()
const rtcStore = useRtcStore() const rtcStore = useRtcStore()
const listRef = ref<HTMLElement>() const listRef = ref<HTMLElement>()
@ -196,7 +194,7 @@ const groupInfo = computed<
} }
}) })
// groupStore map GroupMemberLite @-mention / /** 群成员列表:直接取 groupStore 缓存map 成 GroupMemberLite 给下游消费(@-mention / 邀请等) */
const groupMembers = computed<GroupMemberLite[]>(() => { const groupMembers = computed<GroupMemberLite[]>(() => {
const conversation = conversationStore.activeConversation const conversation = conversationStore.activeConversation
if (!conversation || conversation.type !== ImConversationType.GROUP) { if (!conversation || conversation.type !== ImConversationType.GROUP) {
@ -250,13 +248,11 @@ function reloadGroupData() {
groupStore.fetchGroupMemberList(conversation.targetId, true) groupStore.fetchGroupMemberList(conversation.targetId, true)
} }
/** 历史消息抽屉 ref「聊天历史」icon / 抽屉「查找聊天内容」入口都调 open() 触发 */ const historyDialogRef = ref<InstanceType<typeof MessageHistory>>() // 历史消息抽屉 ref「聊天历史」icon / 抽屉查找聊天内容入口都调 open() 触发
const historyDialogRef = ref<InstanceType<typeof MessageHistory>>()
const sideVisible = ref(false) // 信息抽屉开关:群聊 / 私聊共用一个 ref const sideVisible = ref(false) // 信息抽屉开关:群聊 / 私聊共用一个 ref
const muteMemberDialogRef = ref<InstanceType<typeof GroupMuteMemberDialog>>() const muteMemberDialogRef = ref<InstanceType<typeof GroupMuteMemberDialog>>()
const callMemberPickerRef = ref<InstanceType<typeof RtcCallMemberPickerDialog>>() const callMemberPickerRef = ref<InstanceType<typeof RtcCallMemberPickerDialog>>()
/** 群通话发起:成员选择弹窗打开期间临时持有的 mediaType */ const pendingMediaType = ref<null | number>(null) // mediaType
const pendingMediaType = ref<null | number>(null)
/** 消息右键菜单「禁言」→ 打开时长选择弹窗 */ /** 消息右键菜单「禁言」→ 打开时长选择弹窗 */
function handleMuteMember(groupId: number, userId: number, displayName: string) { function handleMuteMember(groupId: number, userId: number, displayName: string) {
@ -268,8 +264,7 @@ function toggleSide() {
sideVisible.value = !sideVisible.value sideVisible.value = !sideVisible.value
} }
/** 私聊通话入口popover 触发;点 语音 / 视频 直接发起 */ const callPopoverVisible = ref(false) // 私聊通话入口popover 触发;点 语音 / 视频 直接发起
const callPopoverVisible = ref(false)
const callInviting = ref(false) // const callInviting = ref(false) //
async function startPrivateCall(mediaType: number) { async function startPrivateCall(mediaType: number) {
callPopoverVisible.value = false callPopoverVisible.value = false

View File

@ -50,8 +50,7 @@ const emit = defineEmits<{
locate: [messageId: number] locate: [messageId: number]
}>() }>()
/** 文本摘要在引用块里展示的最大字符数 */ const MAX_TEXT_PREVIEW_LEN = 60 //
const MAX_TEXT_PREVIEW_LEN = 60
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const messageStore = useMessageStore() const messageStore = useMessageStore()

View File

@ -5,7 +5,7 @@ import { computed, reactive, ref } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons' import { IconifyIcon as Icon } from '@vben/icons'
import { Button, Input, Modal } from 'ant-design-vue' import { Button, Input, message, Modal } from 'ant-design-vue'
import { createGroup } from '#/api/im/group' import { createGroup } from '#/api/im/group'
import ConversationPickerPanel from '#/views/im/home/components/picker/ConversationPickerPanel.vue' import ConversationPickerPanel from '#/views/im/home/components/picker/ConversationPickerPanel.vue'
@ -29,14 +29,12 @@ import {
removeQuotePayload, removeQuotePayload,
serializeMessage serializeMessage
} from '#/views/im/utils/message' } from '#/views/im/utils/message'
import { useMessage } from '#/views/im/utils/message-feedback'
import { isGroupQuit } from '#/views/im/utils/user' import { isGroupQuit } from '#/views/im/utils/user'
import FacePicker from '../../input/FacePicker.vue' import FacePicker from '../../input/FacePicker.vue'
defineOptions({ name: 'ImMessageForwardDialog' }) defineOptions({ name: 'ImMessageForwardDialog' })
const message = useMessage()
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const friendStore = useFriendStore() const friendStore = useFriendStore()
const groupStore = useGroupStore() const groupStore = useGroupStore()
@ -49,14 +47,12 @@ const state = reactive({
sourceConversation: null as Conversation | null sourceConversation: null as Conversation | null
}) })
const visible = ref(false) const visible = ref(false)
/** 当前视图:默认会话选择,「创建聊天」入口切到好友选择 */ const view = ref<'contact' | 'conversation'>('conversation') //
const view = ref<'contact' | 'conversation'>('conversation')
const selectedKeys = ref<string[]>([]) const selectedKeys = ref<string[]>([])
const selectedFriendIds = ref<number[]>([]) const selectedFriendIds = ref<number[]>([])
const leaveMessage = ref('') const leaveMessage = ref('')
const sending = ref(false) const sending = ref(false)
/** emoji picker 显隐:右侧笑脸按钮切换 */ const emojiVisible = ref(false) // emoji picker
const emojiVisible = ref(false)
defineExpose({ defineExpose({
/** 打开转发弹窗reset → 灌参 → visible=true */ /** 打开转发弹窗reset → 灌参 → visible=true */

View File

@ -17,8 +17,7 @@ defineOptions({ name: 'ImMessageMergeDetailDialog' })
const voicePlayer = useVoicePlayer() const voicePlayer = useVoicePlayer()
const visible = ref(false) const visible = ref(false)
/** 嵌套层级栈,存 parsed payload 避免切层重 parse */ const stack = ref<MergeMessage[]>([]) // parsed payload parse
const stack = ref<MergeMessage[]>([])
defineExpose({ defineExpose({
/** 打开详情弹窗,传入顶层合并消息 content */ /** 打开详情弹窗,传入顶层合并消息 content */

View File

@ -38,10 +38,9 @@ const filteredConversations = computed(() =>
// ==================== ==================== // ==================== ====================
/** 置顶超过该数量时显示折叠入口;以下数量直接铺开(避免单条置顶就出折叠头视觉太重) */ const PINNED_FOLD_THRESHOLD = 3 //
const PINNED_FOLD_THRESHOLD = 3
/** 置顶折叠展开态localStorage 持久化,刷新后保留用户上次的选择,对齐微信 */ // localStorage
const pinnedExpanded = ref( const pinnedExpanded = ref(
localStorage.getItem(StorageKeys.localStorage.conversationPinnedExpanded) === 'true' localStorage.getItem(StorageKeys.localStorage.conversationPinnedExpanded) === 'true'
) )
@ -105,13 +104,11 @@ const showPinnedSection = computed(
// ==================== ==================== // ==================== ====================
/** 添加朋友弹窗 ref右上角 +-下拉「添加朋友」入口调 open() 触发 */ const friendAddDialogRef = ref<InstanceType<typeof FriendAddDialog>>() // ref +- open()
const friendAddDialogRef = ref<InstanceType<typeof FriendAddDialog>>()
// ==================== ==================== // ==================== ====================
/** 发起群聊弹窗 refhandleOpenCreateGroup 调 open() 打开 */ const createGroupDialogRef = ref<InstanceType<typeof GroupCreateDialog>>() // refhandleOpenCreateGroup open()
const createGroupDialogRef = ref<InstanceType<typeof GroupCreateDialog>>()
/** 打开发起群聊弹窗:无锁定项的全局入口 */ /** 打开发起群聊弹窗:无锁定项的全局入口 */
function handleOpenCreateGroup() { function handleOpenCreateGroup() {

View File

@ -1,8 +1,8 @@
import type { ChannelDO } from '../types' import type { ImManagerChannelApi } from '#/api/im/manager/channel'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import { getSimpleChannelList, type ImManagerChannelVO } from '#/api/im/manager/channel' import { getSimpleChannelList } from '#/api/im/manager/channel'
import { ImConversationType } from '../../utils/constants' import { ImConversationType } from '../../utils/constants'
import { getDb } from '../../utils/db' import { getDb } from '../../utils/db'
@ -16,12 +16,12 @@ import { useConversationStore } from './conversationStore'
*/ */
export const useChannelStore = defineStore('imChannelStore', { export const useChannelStore = defineStore('imChannelStore', {
state: () => ({ state: () => ({
channels: [] as ImManagerChannelVO[], channels: [] as ImManagerChannelApi.Channel[],
loaded: false loaded: false
}), }),
getters: { getters: {
getChannel(state): (id: number) => ImManagerChannelVO | undefined { getChannel(state): (id: number) => ImManagerChannelApi.Channel | undefined {
return (id: number) => state.channels.find((c) => c.id === id) return (id: number) => state.channels.find((c) => c.id === id)
} }
}, },
@ -32,7 +32,7 @@ export const useChannelStore = defineStore('imChannelStore', {
/** 从 IndexedDB 恢复频道列表 */ /** 从 IndexedDB 恢复频道列表 */
async loadChannelList(): Promise<boolean> { async loadChannelList(): Promise<boolean> {
try { try {
const cached = await getDb().getAll<ChannelDO>('channels') const cached = await getDb().getAll<ImManagerChannelApi.Channel>('channels')
if (!cached || cached.length === 0) { if (!cached || cached.length === 0) {
return false return false
} }

View File

@ -6,7 +6,7 @@ import type {
MessageDO MessageDO
} from '../types' } from '../types'
import type { ImConversationReadRespVO } from '#/api/im/conversation/read' import type { ImConversationReadApi } from '#/api/im/conversation/read'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
@ -113,7 +113,7 @@ function fromConversationReadDO(record: ConversationReadDO): ConversationRead {
} }
/** 是否为有效会话读位置 */ /** 是否为有效会话读位置 */
function isValidConversationReadRecord(record: ImConversationReadRespVO): boolean { function isValidConversationReadRecord(record: ImConversationReadApi.ConversationReadRespVO): boolean {
return !!record.conversationType && !!record.targetId && !!record.messageId return !!record.conversationType && !!record.targetId && !!record.messageId
} }
@ -351,7 +351,7 @@ export const useConversationStore = defineStore('imConversationStore', {
/** 应用会话读位置 */ /** 应用会话读位置 */
async applyConversationReadList( async applyConversationReadList(
records: ImConversationReadRespVO[], records: ImConversationReadApi.ConversationReadRespVO[],
isActive?: () => boolean isActive?: () => boolean
): Promise<void> { ): Promise<void> {
if (records.length === 0) { if (records.length === 0) {

View File

@ -1,18 +1,12 @@
import type { ImFacePackApi } from '#/api/im/face/pack'
import type { ImFaceUserItemApi } from '#/api/im/face/useritem'
import { ref } from 'vue' import { ref } from 'vue'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import { import { getFacePackList as apiGetFacePackList } from '#/api/im/face/pack'
getFacePackList as apiGetFacePackList, import { createFaceUserItem as apiCreateFaceUserItem, deleteFaceUserItem as apiDeleteFaceUserItem, getFaceUserItemList as apiGetFaceUserItemList } from '#/api/im/face/useritem'
type ImFacePackUserVO
} from '#/api/im/face/pack'
import {
createFaceUserItem as apiCreateFaceUserItem,
deleteFaceUserItem as apiDeleteFaceUserItem,
getFaceUserItemList as apiGetFaceUserItemList,
type ImFaceUserItemSaveReqVO,
type ImFaceUserItemVO
} from '#/api/im/face/useritem'
/** /**
* IM store + * IM store +
@ -24,9 +18,9 @@ import {
export const useFaceStore = defineStore('imFace', () => { export const useFaceStore = defineStore('imFace', () => {
/** 系统表情包列表(含每个包的 items运营管理后台维护 */ /** 系统表情包列表(含每个包的 items运营管理后台维护 */
const facePacks = ref<ImFacePackUserVO[]>([]) const facePacks = ref<ImFacePackApi.FacePackUser[]>([])
/** 个人表情包列表(用户长按「添加到表情」/ 上传产生) */ /** 个人表情包列表(用户长按「添加到表情」/ 上传产生) */
const faceUserItems = ref<ImFaceUserItemVO[]>([]) const faceUserItems = ref<ImFaceUserItemApi.FaceUserItem[]>([])
/** clear() 时递增;旧账号请求返回后不写入新账号内存 */ /** clear() 时递增;旧账号请求返回后不写入新账号内存 */
let storeEpoch = 0 let storeEpoch = 0
@ -89,7 +83,7 @@ export const useFaceStore = defineStore('imFace', () => {
* *
* 1. + 2. * 1. + 2.
*/ */
async function addFaceUserItem(reqVO: ImFaceUserItemSaveReqVO): Promise<boolean> { async function addFaceUserItem(reqVO: ImFaceUserItemApi.FaceUserItemSaveReqVO): Promise<boolean> {
const requestEpoch = storeEpoch const requestEpoch = storeEpoch
const id = await apiCreateFaceUserItem(reqVO) const id = await apiCreateFaceUserItem(reqVO)
if (!id) { if (!id) {

View File

@ -1,29 +1,14 @@
import type { Friend, FriendDO, FriendLite, FriendRequest, FriendRequestDO } from '../types' import type { Friend, FriendLite, FriendRequest } from '../types'
import type { ImFriendApi } from '#/api/im/friend'
import type { ImFriendRequestApi } from '#/api/im/friend/request'
import { CommonStatusEnum } from '@vben/constants' import { CommonStatusEnum } from '@vben/constants'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import { import { blockFriend as apiBlockFriend, deleteFriend as apiDeleteFriend, getFriend as apiGetFriend, getMyFriendList as apiGetMyFriendList, pullMyFriendList as apiPullMyFriendList, unblockFriend as apiUnblockFriend, updateFriend as apiUpdateFriend } from '#/api/im/friend'
blockFriend as apiBlockFriend, import { agreeFriendRequest as apiAgreeFriendRequest, applyFriendRequest as apiApplyFriendRequest, getMyFriendRequest as apiGetMyFriendRequest, getMyFriendRequestList as apiGetMyFriendRequestList, pullMyFriendRequestList as apiPullMyFriendRequestList, refuseFriendRequest as apiRefuseFriendRequest } from '#/api/im/friend/request'
deleteFriend as apiDeleteFriend,
getFriend as apiGetFriend,
getMyFriendList as apiGetMyFriendList,
pullMyFriendList as apiPullMyFriendList,
unblockFriend as apiUnblockFriend,
updateFriend as apiUpdateFriend,
type ImFriendRespVO
} from '#/api/im/friend'
import {
agreeFriendRequest as apiAgreeFriendRequest,
applyFriendRequest as apiApplyFriendRequest,
getMyFriendRequest as apiGetMyFriendRequest,
getMyFriendRequestList as apiGetMyFriendRequestList,
pullMyFriendRequestList as apiPullMyFriendRequestList,
refuseFriendRequest as apiRefuseFriendRequest,
type ImFriendRequestApplyReqVO,
type ImFriendRequestRespVO
} from '#/api/im/friend/request'
import { getCurrentUserId } from '#/views/im/utils/auth' import { getCurrentUserId } from '#/views/im/utils/auth'
import { FRIEND_REQUEST_PAGE_SIZE } from '../../utils/config' import { FRIEND_REQUEST_PAGE_SIZE } from '../../utils/config'
@ -155,8 +140,8 @@ export const useFriendStore = defineStore('imFriendStore', {
async loadFriendData(): Promise<boolean> { async loadFriendData(): Promise<boolean> {
try { try {
const [friends, friendRequests] = await Promise.all([ const [friends, friendRequests] = await Promise.all([
getDb().getAll<FriendDO>('friends'), getDb().getAll<Friend>('friends'),
getDb().getAll<FriendRequestDO>('friendRequests') getDb().getAll<FriendRequest>('friendRequests')
]) ])
if (friends.length > 0) { if (friends.length > 0) {
this.friends = friends this.friends = friends
@ -346,7 +331,7 @@ export const useFriendStore = defineStore('imFriendStore', {
// ==================== 申请-审批 ==================== // ==================== 申请-审批 ====================
/** 发起好友申请:成功后等待对方同意(不直接落地为好友) */ /** 发起好友申请:成功后等待对方同意(不直接落地为好友) */
async applyFriendRequest(reqVO: ImFriendRequestApplyReqVO): Promise<null | number> { async applyFriendRequest(reqVO: ImFriendRequestApi.FriendRequestApplyReqVO): Promise<null | number> {
return await apiApplyFriendRequest(reqVO) return await apiApplyFriendRequest(reqVO)
}, },
@ -816,7 +801,7 @@ export const useFriendStore = defineStore('imFriendStore', {
} }
}) })
function convertFriend(vo: ImFriendRespVO): Friend { function convertFriend(vo: ImFriendApi.FriendRespVO): Friend {
return { return {
id: vo.id, id: vo.id,
friendUserId: vo.friendUserId, friendUserId: vo.friendUserId,
@ -835,7 +820,7 @@ function convertFriend(vo: ImFriendRespVO): Friend {
} }
} }
function convertFriendRequest(vo: ImFriendRequestRespVO): FriendRequest { function convertFriendRequest(vo: ImFriendRequestApi.FriendRequestRespVO): FriendRequest {
return { return {
id: vo.id, id: vo.id,
fromUserId: vo.fromUserId, fromUserId: vo.fromUserId,

View File

@ -1,15 +1,8 @@
import type { GroupRequestDO } from '../types' import type { ImGroupRequestApi } from '#/api/im/group/request'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import { import { agreeGroupRequest as apiAgreeGroupRequest, getMyGroupRequest as apiGetMyGroupRequest, getUnhandledRequestList as apiGetUnhandledRequestList, pullMyGroupRequestList as apiPullMyGroupRequestList, refuseGroupRequest as apiRefuseGroupRequest } from '#/api/im/group/request'
agreeGroupRequest as apiAgreeGroupRequest,
getMyGroupRequest as apiGetMyGroupRequest,
getUnhandledRequestList as apiGetUnhandledRequestList,
pullMyGroupRequestList as apiPullMyGroupRequestList,
refuseGroupRequest as apiRefuseGroupRequest,
type ImGroupRequestRespVO
} from '#/api/im/group/request'
import { getCurrentUserId } from '#/views/im/utils/auth' import { getCurrentUserId } from '#/views/im/utils/auth'
import { ImGroupRequestHandleResult } from '#/views/im/utils/constants' import { ImGroupRequestHandleResult } from '#/views/im/utils/constants'
@ -26,7 +19,7 @@ let pendingUnhandledFetch: null | PendingRequest = null
* IM Store * IM Store
* *
* unhandledList * unhandledList
* / Drawer count ImGroupRespVO pendingRequestCount * / Drawer count ImGroupApi.GroupRespVO pendingRequestCount
* *
* *
* - IM fetchUnhandledGroupRequestList * - IM fetchUnhandledGroupRequestList
@ -38,7 +31,7 @@ let pendingUnhandledFetch: null | PendingRequest = null
export const useGroupRequestStore = defineStore('imGroupRequestStore', { export const useGroupRequestStore = defineStore('imGroupRequestStore', {
state: () => ({ state: () => ({
/** 我管理的所有群下未处理申请列表(按 id 倒序) */ /** 我管理的所有群下未处理申请列表(按 id 倒序) */
unhandledList: [] as ImGroupRequestRespVO[], unhandledList: [] as ImGroupRequestApi.GroupRequestRespVO[],
/** fetchUnhandledGroupRequestList 是否成功执行过;避免横幅显示 0 然后跳数字的闪烁 */ /** fetchUnhandledGroupRequestList 是否成功执行过;避免横幅显示 0 然后跳数字的闪烁 */
loaded: false loaded: false
}), }),
@ -61,7 +54,7 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
/** 指定群下的未处理申请列表 */ /** 指定群下的未处理申请列表 */
getUnhandledGroupRequestListByGroupId: getUnhandledGroupRequestListByGroupId:
(state) => (state) =>
(groupId: number): ImGroupRequestRespVO[] => (groupId: number): ImGroupRequestApi.GroupRequestRespVO[] =>
state.unhandledList.filter((r) => r.groupId === groupId) state.unhandledList.filter((r) => r.groupId === groupId)
}, },
@ -69,13 +62,13 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
/** 从 IndexedDB 恢复加群申请 */ /** 从 IndexedDB 恢复加群申请 */
async loadGroupRequestList(): Promise<boolean> { async loadGroupRequestList(): Promise<boolean> {
try { try {
const cached = await getDb().getAll<GroupRequestDO>('groupRequests') const cached = await getDb().getAll<ImGroupRequestApi.GroupRequestRespVO>('groupRequests')
if (!cached || cached.length === 0) { if (!cached || cached.length === 0) {
return false return false
} }
this.unhandledList = cached this.unhandledList = cached
.filter((request) => request.handleResult === ImGroupRequestHandleResult.UNHANDLED) .filter((request) => request.handleResult === ImGroupRequestHandleResult.UNHANDLED)
.sort((requestA, requestB) => requestB.id - requestA.id) .toSorted((requestA, requestB) => requestB.id - requestA.id)
return true return true
} catch (error) { } catch (error) {
console.warn('[IM groupRequestStore] 本地加群申请缓存读取失败', error) console.warn('[IM groupRequestStore] 本地加群申请缓存读取失败', error)
@ -97,12 +90,12 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
}, },
/** 保存单条加群申请 */ /** 保存单条加群申请 */
async saveGroupRequestRecord(request: ImGroupRequestRespVO): Promise<void> { async saveGroupRequestRecord(request: ImGroupRequestApi.GroupRequestRespVO): Promise<void> {
await getDb().put('groupRequests', request) await getDb().put('groupRequests', request)
}, },
/** 保存单条加群申请 */ /** 保存单条加群申请 */
saveGroupRequest(request: ImGroupRequestRespVO): void { saveGroupRequest(request: ImGroupRequestApi.GroupRequestRespVO): void {
void this.saveGroupRequestRecord(request).catch((error) => void this.saveGroupRequestRecord(request).catch((error) =>
console.warn('[IM groupRequestStore] 本地加群申请写入失败', error) console.warn('[IM groupRequestStore] 本地加群申请写入失败', error)
) )
@ -161,14 +154,14 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
* *
* id / * id /
*/ */
upsertGroupRequest(request: ImGroupRequestRespVO) { upsertGroupRequest(request: ImGroupRequestApi.GroupRequestRespVO) {
void this.upsertGroupRequestForPull(request).catch((error) => void this.upsertGroupRequestForPull(request).catch((error) =>
console.warn('[IM groupRequestStore] 本地加群申请写入失败', error) console.warn('[IM groupRequestStore] 本地加群申请写入失败', error)
) )
}, },
/** 本地合并 / 新增单条加群申请 */ /** 本地合并 / 新增单条加群申请 */
async upsertGroupRequestForPull(request: ImGroupRequestRespVO): Promise<void> { async upsertGroupRequestForPull(request: ImGroupRequestApi.GroupRequestRespVO): Promise<void> {
if (request.handleResult !== ImGroupRequestHandleResult.UNHANDLED) { if (request.handleResult !== ImGroupRequestHandleResult.UNHANDLED) {
await this.removeGroupRequestByIdForPull(request.id) await this.removeGroupRequestByIdForPull(request.id)
return return

View File

@ -1,20 +1,14 @@
import type { Group, GroupDO, GroupMember, GroupMemberDO, Message } from '../types' import type { Group, GroupDO, GroupMember, Message } from '../types'
import type { ImGroupApi } from '#/api/im/group'
import type { ImGroupMemberApi } from '#/api/im/group/member'
import { CommonStatusEnum } from '@vben/constants' import { CommonStatusEnum } from '@vben/constants'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import { import { getGroup as apiGetGroup, getMyGroupList as apiGetMyGroupList } from '#/api/im/group'
getGroup as apiGetGroup, import { getGroupMember as apiGetGroupMember, getGroupMemberList as apiGetGroupMemberList, updateGroupMember as apiUpdateGroupMember } from '#/api/im/group/member'
getMyGroupList as apiGetMyGroupList,
type ImGroupRespVO
} from '#/api/im/group'
import {
getGroupMember as apiGetGroupMember,
getGroupMemberList as apiGetGroupMemberList,
updateGroupMember as apiUpdateGroupMember,
type ImGroupMemberRespVO
} from '#/api/im/group/member'
import { getCurrentUserId } from '#/views/im/utils/auth' import { getCurrentUserId } from '#/views/im/utils/auth'
import { import {
@ -159,7 +153,7 @@ export const useGroupStore = defineStore('imGroupStore', {
return cachedGroup.members return cachedGroup.members
} }
try { try {
const cached = await getDb().getAllByIndex<GroupMemberDO>( const cached = await getDb().getAllByIndex<GroupMember>(
'groupMembers', 'groupMembers',
'groupId', 'groupId',
groupId groupId
@ -229,7 +223,7 @@ export const useGroupStore = defineStore('imGroupStore', {
return return
} }
const fresh = (list || []).map((group) => convertGroup(group)) const fresh = (list || []).map((group) => convertGroup(group))
// 合并而非全量替换silent / groupRemark / 成员缓存这些字段不在 ImGroupRespVO 里,得从旧 group 保留 // 合并而非全量替换silent / groupRemark / 成员缓存这些字段不在 ImGroupApi.GroupRespVO 里,得从旧 group 保留
const groupMap = new Map(this.groups.map((group) => [group.id, group])) const groupMap = new Map(this.groups.map((group) => [group.id, group]))
this.groups = fresh.map((group) => { this.groups = fresh.map((group) => {
const existing = groupMap.get(group.id) const existing = groupMap.get(group.id)
@ -314,7 +308,7 @@ export const useGroupStore = defineStore('imGroupStore', {
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
return [] return []
} }
let meRaw: ImGroupMemberRespVO | undefined let meRaw: ImGroupMemberApi.GroupMemberRespVO | undefined
const members = (list || []).map((member) => { const members = (list || []).map((member) => {
if (member.userId === requestUserId) { if (member.userId === requestUserId) {
meRaw = member meRaw = member
@ -903,7 +897,7 @@ export const useGroupStore = defineStore('imGroupStore', {
} }
}) })
function convertGroup(group: ImGroupRespVO): Group { function convertGroup(group: ImGroupApi.GroupRespVO): Group {
return { return {
id: group.id, id: group.id,
name: group.name, name: group.name,
@ -918,9 +912,9 @@ function convertGroup(group: ImGroupRespVO): Group {
} }
} }
/** 后端 ImGroupMessageRespVO -> 前端 Message补 targetId / selfSend / sendTime 等派生字段 */ /** 后端 ImGroupMessageApi.GroupMessageRespVO -> 前端 Message补 targetId / selfSend / sendTime 等派生字段 */
function convertGroupMessageVO( function convertGroupMessageVO(
message: NonNullable<ImGroupRespVO['pinnedMessages']>[number] message: NonNullable<ImGroupApi.GroupRespVO['pinnedMessages']>[number]
): Message { ): Message {
const currentUserId = getCurrentUserId() const currentUserId = getCurrentUserId()
return { return {
@ -940,7 +934,7 @@ function convertGroupMessageVO(
} }
} }
function convertGroupMember(member: ImGroupMemberRespVO, groupId: number): GroupMember { function convertGroupMember(member: ImGroupMemberApi.GroupMemberRespVO, groupId: number): GroupMember {
return { return {
id: member.id, id: member.id,
userId: member.userId, userId: member.userId,

View File

@ -1,4 +1,4 @@
import type { ImRtcCallRespVO, ImRtcGroupCallRespVO } from '#/api/im/rtc' import type { ImRtcApi } from '#/api/im/rtc'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
@ -74,7 +74,7 @@ export const useRtcStore = defineStore('imRtc', () => {
/** 当前阶段 */ /** 当前阶段 */
const stage = ref<ImRtcCallStageValue>(ImRtcCallStage.IDLE) const stage = ref<ImRtcCallStageValue>(ImRtcCallStage.IDLE)
/** 当前通话invite / accept / refreshToken 拿到的完整信息 */ /** 当前通话invite / accept / refreshToken 拿到的完整信息 */
const call = ref<ImRtcCallRespVO | null>(null) const call = ref<ImRtcApi.RtcCallRespVO | null>(null)
/** 来电载荷;仅 INCOMING 阶段使用status 固定 INVITING其它字段 INVITE 专属 */ /** 来电载荷;仅 INCOMING 阶段使用status 固定 INVITING其它字段 INVITE 专属 */
const incomingPayload = ref<ImRtcCallNotification | null>(null) const incomingPayload = ref<ImRtcCallNotification | null>(null)
/** 进入 RUNNING 的时间戳用于通话时长展示reset 时清零 */ /** 进入 RUNNING 的时间戳用于通话时长展示reset 时清零 */
@ -115,13 +115,13 @@ export const useRtcStore = defineStore('imRtc', () => {
}) })
/** 私聊场景对端 userId自己是主叫则取首个 invitee否则取 inviter */ /** 私聊场景对端 userId自己是主叫则取首个 invitee否则取 inviter */
function resolvePrivatePeerUserId(c: ImRtcCallRespVO): number | undefined { function resolvePrivatePeerUserId(c: ImRtcApi.RtcCallRespVO): number | undefined {
const myId = getCurrentUserId() const myId = getCurrentUserId()
return c.inviterId === myId ? c.inviteeIds?.[0] : c.inviterId return c.inviterId === myId ? c.inviteeIds?.[0] : c.inviterId
} }
/** 群活跃通话索引groupId -> 群通话摘要;用于群聊顶部胶囊条 */ /** 群活跃通话索引groupId -> 群通话摘要;用于群聊顶部胶囊条 */
const groupActiveCalls = ref<Map<number, ImRtcGroupCallRespVO>>(new Map()) const groupActiveCalls = ref<Map<number, ImRtcApi.RtcGroupCallRespVO>>(new Map())
/** /**
* 退 / pending * 退 / pending
@ -147,7 +147,7 @@ export const useRtcStore = defineStore('imRtc', () => {
* RUNNING * RUNNING
* status RUNNING RUNNINGCREATED INVITING * status RUNNING RUNNINGCREATED INVITING
*/ */
function startInviting(data: ImRtcCallRespVO) { function startInviting(data: ImRtcApi.RtcCallRespVO) {
call.value = data call.value = data
// 群通话场景写入本地胶囊条缓存 // 群通话场景写入本地胶囊条缓存
syncGroupActiveCall(data) syncGroupActiveCall(data)
@ -184,7 +184,7 @@ export const useRtcStore = defineStore('imRtc', () => {
} }
/** 进入通话中阶段 */ /** 进入通话中阶段 */
function enterRunning(data: ImRtcCallRespVO) { function enterRunning(data: ImRtcApi.RtcCallRespVO) {
call.value = data call.value = data
// 离开 INCOMING 阶段;清空来电载荷 // 离开 INCOMING 阶段;清空来电载荷
incomingPayload.value = null incomingPayload.value = null
@ -252,7 +252,7 @@ export const useRtcStore = defineStore('imRtc', () => {
* LiveKit ParticipantConnected / Disconnected * LiveKit ParticipantConnected / Disconnected
* joinedUserIds / inviteeIds / getActiveCall * joinedUserIds / inviteeIds / getActiveCall
*/ */
function setGroupCall(payload: ImRtcGroupCallRespVO) { function setGroupCall(payload: ImRtcApi.RtcGroupCallRespVO) {
if (!payload?.groupId) { if (!payload?.groupId) {
return return
} }
@ -267,7 +267,7 @@ export const useRtcStore = defineStore('imRtc', () => {
} }
/** 两条群通话摘要内容相等room / mediaType / inviterId / 两个 userId 数组逐项相等) */ /** 两条群通话摘要内容相等room / mediaType / inviterId / 两个 userId 数组逐项相等) */
function isSameGroupCall(a: ImRtcGroupCallRespVO, b: ImRtcGroupCallRespVO): boolean { function isSameGroupCall(a: ImRtcApi.RtcGroupCallRespVO, b: ImRtcApi.RtcGroupCallRespVO): boolean {
if (a.room !== b.room || a.mediaType !== b.mediaType || a.inviterId !== b.inviterId) { if (a.room !== b.room || a.mediaType !== b.mediaType || a.inviterId !== b.inviterId) {
return false return false
} }
@ -294,7 +294,7 @@ export const useRtcStore = defineStore('imRtc', () => {
} }
/** 获取群当前活跃通话;用于胶囊条按 groupId 查询 */ /** 获取群当前活跃通话;用于胶囊条按 groupId 查询 */
function getGroupCall(groupId: number): ImRtcGroupCallRespVO | undefined { function getGroupCall(groupId: number): ImRtcApi.RtcGroupCallRespVO | undefined {
return groupActiveCalls.value.get(groupId) return groupActiveCalls.value.get(groupId)
} }

View File

@ -10,7 +10,7 @@ import type {
WebSocketFrame WebSocketFrame
} from '../types' } from '../types'
import type { ImChannelMessageRespVO } from '#/api/im/message/channel' import type { ImChannelMessageApi } from '#/api/im/message/channel'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
@ -215,7 +215,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
reconnectAttempts: 0, reconnectAttempts: 0,
heartbeatTimer: null as null | ReturnType<typeof setInterval>, heartbeatTimer: null as null | ReturnType<typeof setInterval>,
messageBuffer: [] as Array< messageBuffer: [] as Array<
| { conversationType: typeof ImConversationType.CHANNEL; payload: ImChannelMessageRespVO } | { conversationType: typeof ImConversationType.CHANNEL; payload: ImChannelMessageApi.ChannelMessageRespVO }
| { | {
conversationType: typeof ImConversationType.GROUP conversationType: typeof ImConversationType.GROUP
payload: ImGroupMessageNotification payload: ImGroupMessageNotification
@ -345,7 +345,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
} }
switch (notification.conversationType) { switch (notification.conversationType) {
case ImConversationType.CHANNEL: { case ImConversationType.CHANNEL: {
this.dispatchChannelFrame(payload as ImChannelMessageRespVO) this.dispatchChannelFrame(payload as ImChannelMessageApi.ChannelMessageRespVO)
break break
} }
case ImConversationType.GROUP: { case ImConversationType.GROUP: {
@ -394,7 +394,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
/** /**
* payload.type READ * payload.type READ
*/ */
dispatchChannelFrame(websocketMessage: ImChannelMessageRespVO) { dispatchChannelFrame(websocketMessage: ImChannelMessageApi.ChannelMessageRespVO) {
if (websocketMessage.type === ImContentType.READ) { if (websocketMessage.type === ImContentType.READ) {
this.handleChannelRead(websocketMessage) this.handleChannelRead(websocketMessage)
return return
@ -403,7 +403,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
}, },
/** 频道 READ自己其它终端在某频道里标为已读本端同步清零该频道未读 */ /** 频道 READ自己其它终端在某频道里标为已读本端同步清零该频道未读 */
handleChannelRead(websocketMessage: ImChannelMessageRespVO) { handleChannelRead(websocketMessage: ImChannelMessageApi.ChannelMessageRespVO) {
void useConversationStore() void useConversationStore()
.applyConversationReadList([ .applyConversationReadList([
{ {
@ -420,7 +420,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
* + insertMessage * + insertMessage
* pull WS id messageStore.insertMessage id * pull WS id messageStore.insertMessage id
*/ */
handleChannelMessage(websocketMessage: ImChannelMessageRespVO): Promise<void> { handleChannelMessage(websocketMessage: ImChannelMessageApi.ChannelMessageRespVO): Promise<void> {
const conversationStore = useConversationStore() const conversationStore = useConversationStore()
const messageStore = useMessageStore() const messageStore = useMessageStore()
// 离线加载期间先缓冲,等 pull 完成后再统一回放,避免重复或顺序错乱 // 离线加载期间先缓冲,等 pull 完成后再统一回放,避免重复或顺序错乱

View File

@ -178,7 +178,7 @@ export interface SettingDO<T = unknown> {
// 群实体(前端内部结构) // 群实体(前端内部结构)
export interface Group { export interface Group {
// ========== 后端字段(对齐 ImGroupRespVO ========== // ========== 后端字段(对齐 ImGroupApi.GroupRespVO ==========
id: number // 群编号 id: number // 群编号
name: string // 群名称 name: string // 群名称
avatar?: string // 群头像 avatar?: string // 群头像
@ -203,7 +203,7 @@ export type GroupDO = Omit<Group, 'members' | 'membersExpired' | 'membersLoaded'
// 群成员实体(前端内部结构) // 群成员实体(前端内部结构)
export interface GroupMember { export interface GroupMember {
// ========== 后端字段(对齐 ImGroupMemberRespVO ========== // ========== 后端字段(对齐 ImGroupMemberApi.GroupMemberRespVO ==========
id?: number // 群成员关系记录编号 id?: number // 群成员关系记录编号
groupId: number // 群编号 groupId: number // 群编号
userId: number // 用户编号 userId: number // 用户编号
@ -218,13 +218,11 @@ export interface GroupMember {
isOwner?: boolean // 是否群主(前端从 Group.ownerUserId 计算) isOwner?: boolean // 是否群主(前端从 Group.ownerUserId 计算)
} }
export type GroupMemberDO = GroupMember
// ==================== 好友 ==================== // ==================== 好友 ====================
// 好友实体(前端内部结构) // 好友实体(前端内部结构)
export interface Friend { export interface Friend {
// ========== 后端字段(对齐 ImFriendRespVO ========== // ========== 后端字段(对齐 ImFriendApi.FriendRespVO ==========
id?: number // 好友关系记录编号(本地乐观新增时可能暂缺) id?: number // 好友关系记录编号(本地乐观新增时可能暂缺)
friendUserId: number // 好友用户编号(与 Conversation.targetId 对齐) friendUserId: number // 好友用户编号(与 Conversation.targetId 对齐)
nickname: string // 好友昵称对方真实昵称永远不被备注覆盖UI 显示走 displayName || nickname nickname: string // 好友昵称对方真实昵称永远不被备注覆盖UI 显示走 displayName || nickname
@ -241,13 +239,11 @@ export interface Friend {
deleteTime?: number // 删除好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换) deleteTime?: number // 删除好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换)
} }
export type FriendDO = Friend
/** /**
* ImFriendRequestRespVO * ImFriendRequestApi.FriendRequestRespVO
*/ */
export interface FriendRequest { export interface FriendRequest {
// ========== 后端字段(对齐 ImFriendRequestRespVO ========== // ========== 后端字段(对齐 ImFriendRequestApi.FriendRequestRespVO ==========
id: number // 申请编号 id: number // 申请编号
fromUserId: number // 发起方用户编号 fromUserId: number // 发起方用户编号
toUserId: number // 接收方用户编号 toUserId: number // 接收方用户编号
@ -265,12 +261,6 @@ export interface FriendRequest {
toAvatar?: string // 接收方头像 toAvatar?: string // 接收方头像
} }
export type FriendRequestDO = FriendRequest
export type GroupRequestDO = import('#/api/im/group/request').ImGroupRequestRespVO
export type ChannelDO = import('#/api/im/manager/channel').ImManagerChannelVO
// ==================== 用户名片 ==================== // ==================== 用户名片 ====================
// 用户精简信息(对齐后端 UserSimpleRespVO名片 / 头像 hover 等场景共用) // 用户精简信息(对齐后端 UserSimpleRespVO名片 / 头像 hover 等场景共用)

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table' import type { VxeTableGridOptions } from '#/adapter/vxe-table'
import type { ImManagerChannelVO } from '#/api/im/manager/channel' import type { ImManagerChannelApi } from '#/api/im/manager/channel'
import { Page, useVbenModal } from '@vben/common-ui' import { Page, useVbenModal } from '@vben/common-ui'
@ -31,12 +31,12 @@ function handleCreate() {
} }
/** 编辑频道 */ /** 编辑频道 */
function handleEdit(row: ImManagerChannelVO) { function handleEdit(row: ImManagerChannelApi.Channel) {
formModalApi.setData(row).open() formModalApi.setData(row).open()
} }
/** 删除频道 */ /** 删除频道 */
async function handleDelete(row: ImManagerChannelVO) { async function handleDelete(row: ImManagerChannelApi.Channel) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0 duration: 0
@ -77,7 +77,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true search: true
} }
} as VxeTableGridOptions<ImManagerChannelVO> } as VxeTableGridOptions<ImManagerChannelApi.Channel>
}) })
</script> </script>

View File

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ImManagerChannelVO } from '#/api/im/manager/channel' import type { ImManagerChannelApi } from '#/api/im/manager/channel'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
@ -18,7 +18,7 @@ import { $t } from '#/locales'
import { useFormSchema } from '../data' import { useFormSchema } from '../data'
const emit = defineEmits(['success']) const emit = defineEmits(['success'])
const formData = ref<ImManagerChannelVO>() const formData = ref<ImManagerChannelApi.Channel>()
const getTitle = computed(() => { const getTitle = computed(() => {
return formData.value?.id return formData.value?.id
? $t('ui.actionTitle.edit', ['频道']) ? $t('ui.actionTitle.edit', ['频道'])
@ -45,7 +45,7 @@ const [Modal, modalApi] = useVbenModal({
return return
} }
modalApi.lock() modalApi.lock()
const data = (await formApi.getValues()) as ImManagerChannelVO const data = (await formApi.getValues()) as ImManagerChannelApi.Channel
try { try {
await (formData.value?.id ? updateManagerChannel(data) : createManagerChannel(data)) await (formData.value?.id ? updateManagerChannel(data) : createManagerChannel(data))
await modalApi.close() await modalApi.close()
@ -60,7 +60,7 @@ const [Modal, modalApi] = useVbenModal({
formData.value = undefined formData.value = undefined
return return
} }
const data = modalApi.getData<ImManagerChannelVO>() const data = modalApi.getData<ImManagerChannelApi.Channel>()
if (!data || !data.id) { if (!data || !data.id) {
return return
} }

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ImManagerChannelMaterialVO } from '#/api/im/manager/channel/material'; import type { ImManagerChannelMaterialApi } from '#/api/im/manager/channel/material';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
@ -34,12 +34,12 @@ function handleCreate() {
} }
/** 编辑素材 */ /** 编辑素材 */
function handleEdit(row: ImManagerChannelMaterialVO) { function handleEdit(row: ImManagerChannelMaterialApi.Material) {
formModalApi.setData(row).open(); formModalApi.setData(row).open();
} }
/** 删除素材 */ /** 删除素材 */
async function handleDelete(row: ImManagerChannelMaterialVO) { async function handleDelete(row: ImManagerChannelMaterialApi.Material) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.title]), content: $t('ui.actionMessage.deleting', [row.title]),
duration: 0, duration: 0,
@ -80,7 +80,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<ImManagerChannelMaterialVO>, } as VxeTableGridOptions<ImManagerChannelMaterialApi.Material>,
}); });
</script> </script>

View File

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ImManagerChannelMaterialVO } from '#/api/im/manager/channel/material'; import type { ImManagerChannelMaterialApi } from '#/api/im/manager/channel/material';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
@ -18,7 +18,7 @@ import { $t } from '#/locales';
import { useFormSchema } from '../data'; import { useFormSchema } from '../data';
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formData = ref<ImManagerChannelMaterialVO>(); const formData = ref<ImManagerChannelMaterialApi.Material>();
const getTitle = computed(() => { const getTitle = computed(() => {
return formData.value?.id return formData.value?.id
? $t('ui.actionTitle.edit', ['素材']) ? $t('ui.actionTitle.edit', ['素材'])
@ -46,7 +46,7 @@ const [Modal, modalApi] = useVbenModal({
} }
modalApi.lock(); modalApi.lock();
const data = const data =
(await formApi.getValues()) as ImManagerChannelMaterialVO; (await formApi.getValues()) as ImManagerChannelMaterialApi.Material;
try { try {
await (formData.value?.id await (formData.value?.id
? updateManagerChannelMaterial(data) ? updateManagerChannelMaterial(data)
@ -64,7 +64,7 @@ const [Modal, modalApi] = useVbenModal({
await formApi.resetForm(); await formApi.resetForm();
return; return;
} }
const data = modalApi.getData<ImManagerChannelMaterialVO>(); const data = modalApi.getData<ImManagerChannelMaterialApi.Material>();
await formApi.setValues({ type: 1 }); await formApi.setValues({ type: 1 });
if (!data || !data.id) { if (!data || !data.id) {
return; return;

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ImManagerChannelMessageVO } from '#/api/im/manager/channel/message'; import type { ImManagerChannelMessageApi } from '#/api/im/manager/channel/message';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
@ -34,7 +34,7 @@ function handleSend() {
} }
/** 删除频道消息 */ /** 删除频道消息 */
async function handleDelete(row: ImManagerChannelMessageVO) { async function handleDelete(row: ImManagerChannelMessageApi.ChannelMessage) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]), content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0, duration: 0,
@ -75,7 +75,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<ImManagerChannelMessageVO>, } as VxeTableGridOptions<ImManagerChannelMessageApi.ChannelMessage>,
}); });
</script> </script>

View File

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ImManagerChannelVO } from '#/api/im/manager/channel'; import type { ImManagerChannelApi } from '#/api/im/manager/channel';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
@ -29,7 +29,7 @@ const emit = defineEmits<{
}>(); }>();
const loading = ref(false); const loading = ref(false);
const channelList = ref<ImManagerChannelVO[]>([]); const channelList = ref<ImManagerChannelApi.Channel[]>([]);
const value = computed({ const value = computed({
get: () => props.modelValue, get: () => props.modelValue,

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ImManagerGroupVO } from '#/api/im/manager/group'; import type { ImManagerGroupApi } from '#/api/im/manager/group';
import { computed, ref, watch } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { Select } from 'ant-design-vue'; import { Select } from 'ant-design-vue';
@ -29,7 +29,7 @@ const emit = defineEmits<{
}>(); }>();
const loading = ref(false); const loading = ref(false);
const groupList = ref<ImManagerGroupVO[]>([]); const groupList = ref<ImManagerGroupApi.Group[]>([]);
const value = computed({ const value = computed({
get: () => props.modelValue, get: () => props.modelValue,
@ -57,7 +57,7 @@ async function loadGroupList() {
} }
} }
watch(() => props.modelValue, loadGroupList, { immediate: true }); onMounted(loadGroupList);
</script> </script>
<template> <template>

View File

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ImManagerChannelMaterialVO } from '#/api/im/manager/channel/material'; import type { ImManagerChannelMaterialApi } from '#/api/im/manager/channel/material';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
@ -29,7 +29,7 @@ const emit = defineEmits<{
}>(); }>();
const loading = ref(false); const loading = ref(false);
const materialList = ref<ImManagerChannelMaterialVO[]>([]); const materialList = ref<ImManagerChannelMaterialApi.Material[]>([]);
const value = computed({ const value = computed({
get: () => props.modelValue, get: () => props.modelValue,

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ImManagerFacePackVO } from '#/api/im/manager/face/pack'; import type { ImManagerFacePackApi } from '#/api/im/manager/face/pack';
import { ref } from 'vue'; import { ref } from 'vue';
@ -45,17 +45,17 @@ function handleCreate() {
} }
/** 编辑表情包 */ /** 编辑表情包 */
function handleEdit(row: ImManagerFacePackVO) { function handleEdit(row: ImManagerFacePackApi.FacePack) {
formModalApi.setData(row).open(); formModalApi.setData(row).open();
} }
/** 打开表情管理 */ /** 打开表情管理 */
function handleItems(row: ImManagerFacePackVO) { function handleItems(row: ImManagerFacePackApi.FacePack) {
itemDrawerRef.value?.open(row); itemDrawerRef.value?.open(row);
} }
/** 删除表情包 */ /** 删除表情包 */
async function handleDelete(row: ImManagerFacePackVO) { async function handleDelete(row: ImManagerFacePackApi.FacePack) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0, duration: 0,
@ -89,7 +89,7 @@ async function handleDeleteBatch() {
function handleRowCheckboxChange({ function handleRowCheckboxChange({
records, records,
}: { }: {
records: ImManagerFacePackVO[]; records: ImManagerFacePackApi.FacePack[];
}) { }) {
checkedIds.value = records.map((item) => item.id); checkedIds.value = records.map((item) => item.id);
} }
@ -121,7 +121,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<ImManagerFacePackVO>, } as VxeTableGridOptions<ImManagerFacePackApi.FacePack>,
gridEvents: { gridEvents: {
checkboxAll: handleRowCheckboxChange, checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange, checkboxChange: handleRowCheckboxChange,

View File

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ImManagerFacePackVO } from '#/api/im/manager/face/pack'; import type { ImManagerFacePackApi } from '#/api/im/manager/face/pack';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
@ -18,7 +18,7 @@ import { $t } from '#/locales';
import { usePackFormSchema } from '../data'; import { usePackFormSchema } from '../data';
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formData = ref<ImManagerFacePackVO>(); const formData = ref<ImManagerFacePackApi.FacePack>();
const getTitle = computed(() => { const getTitle = computed(() => {
return formData.value?.id ? '修改表情包' : '新增表情包'; return formData.value?.id ? '修改表情包' : '新增表情包';
}); });
@ -43,7 +43,7 @@ const [Modal, modalApi] = useVbenModal({
return; return;
} }
modalApi.lock(); modalApi.lock();
const data = (await formApi.getValues()) as ImManagerFacePackVO; const data = (await formApi.getValues()) as ImManagerFacePackApi.FacePack;
try { try {
await (formData.value?.id await (formData.value?.id
? updateManagerFacePack(data) ? updateManagerFacePack(data)
@ -61,7 +61,7 @@ const [Modal, modalApi] = useVbenModal({
await formApi.resetForm(); await formApi.resetForm();
return; return;
} }
const data = modalApi.getData<ImManagerFacePackVO>(); const data = modalApi.getData<ImManagerFacePackApi.FacePack>();
if (!data || !data.id) { if (!data || !data.id) {
return; return;
} }

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ImManagerFacePackItemVO } from '#/api/im/manager/face/item'; import type { ImManagerFacePackItemApi } from '#/api/im/manager/face/item';
import type { ImManagerFacePackVO } from '#/api/im/manager/face/pack'; import type { ImManagerFacePackApi } from '#/api/im/manager/face/pack';
import { computed, nextTick, ref } from 'vue'; import { computed, nextTick, ref } from 'vue';
@ -22,7 +22,7 @@ import { useItemGridColumns, useItemGridFormSchema } from '../data';
import ItemForm from './item-form.vue'; import ItemForm from './item-form.vue';
const visible = ref(false); const visible = ref(false);
const currentPack = ref<ImManagerFacePackVO>(); const currentPack = ref<ImManagerFacePackApi.FacePack>();
const checkedIds = ref<number[]>([]); const checkedIds = ref<number[]>([]);
const title = computed(() => const title = computed(() =>
currentPack.value ? `${currentPack.value.name}」表情管理` : '表情管理', currentPack.value ? `${currentPack.value.name}」表情管理` : '表情管理',
@ -34,7 +34,7 @@ const [ItemFormModal, itemFormModalApi] = useVbenModal({
}); });
/** 打开抽屉 */ /** 打开抽屉 */
function open(pack: ImManagerFacePackVO) { function open(pack: ImManagerFacePackApi.FacePack) {
currentPack.value = pack; currentPack.value = pack;
visible.value = true; visible.value = true;
checkedIds.value = []; checkedIds.value = [];
@ -62,14 +62,14 @@ function handleCreate() {
} }
/** 编辑表情 */ /** 编辑表情 */
function handleEdit(row: ImManagerFacePackItemVO) { function handleEdit(row: ImManagerFacePackItemApi.FacePackItem) {
itemFormModalApi itemFormModalApi
.setData({ id: row.id, packId: currentPack.value?.id }) .setData({ id: row.id, packId: currentPack.value?.id })
.open(); .open();
} }
/** 删除表情 */ /** 删除表情 */
async function handleDelete(row: ImManagerFacePackItemVO) { async function handleDelete(row: ImManagerFacePackItemApi.FacePackItem) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name || row.id]), content: $t('ui.actionMessage.deleting', [row.name || row.id]),
duration: 0, duration: 0,
@ -103,7 +103,7 @@ async function handleDeleteBatch() {
function handleRowCheckboxChange({ function handleRowCheckboxChange({
records, records,
}: { }: {
records: ImManagerFacePackItemVO[]; records: ImManagerFacePackItemApi.FacePackItem[];
}) { }) {
checkedIds.value = records.map((item) => item.id); checkedIds.value = records.map((item) => item.id);
} }
@ -136,7 +136,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<ImManagerFacePackItemVO>, } as VxeTableGridOptions<ImManagerFacePackItemApi.FacePackItem>,
gridEvents: { gridEvents: {
checkboxAll: handleRowCheckboxChange, checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange, checkboxChange: handleRowCheckboxChange,

View File

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ImManagerFacePackItemVO } from '#/api/im/manager/face/item'; import type { ImManagerFacePackItemApi } from '#/api/im/manager/face/item';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
@ -19,7 +19,7 @@ import { probeImageSize } from '#/views/im/manager/utils/format';
import { useItemFormSchema } from '../data'; import { useItemFormSchema } from '../data';
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formData = ref<ImManagerFacePackItemVO>(); const formData = ref<ImManagerFacePackItemApi.FacePackItem>();
const packId = ref(0); const packId = ref(0);
const lastUrl = ref(''); const lastUrl = ref('');
const getTitle = computed(() => { const getTitle = computed(() => {
@ -41,7 +41,13 @@ const [Form, formApi] = useVbenForm({
/** 回填图片尺寸 */ /** 回填图片尺寸 */
async function applyImageSize(url?: string) { async function applyImageSize(url?: string) {
if (!url || url === lastUrl.value) { if (!url) {
lastUrl.value = '';
await formApi.setFieldValue('width', undefined);
await formApi.setFieldValue('height', undefined);
return;
}
if (url === lastUrl.value) {
return; return;
} }
lastUrl.value = url; lastUrl.value = url;
@ -68,7 +74,7 @@ const [Modal, modalApi] = useVbenModal({
return; return;
} }
modalApi.lock(); modalApi.lock();
const data = (await formApi.getValues()) as ImManagerFacePackItemVO; const data = (await formApi.getValues()) as ImManagerFacePackItemApi.FacePackItem;
try { try {
data.packId = data.packId || packId.value; data.packId = data.packId || packId.value;
await (formData.value?.id await (formData.value?.id

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ImManagerFaceUserItemVO } from '#/api/im/manager/face/useritem'; import type { ImManagerFaceUserItemApi } from '#/api/im/manager/face/useritem';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
@ -24,7 +24,7 @@ function handleRefresh() {
} }
/** 删除用户表情 */ /** 删除用户表情 */
async function handleDelete(row: ImManagerFaceUserItemVO) { async function handleDelete(row: ImManagerFaceUserItemApi.FaceUserItem) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name || row.id]), content: $t('ui.actionMessage.deleting', [row.name || row.id]),
duration: 0, duration: 0,
@ -65,7 +65,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<ImManagerFaceUserItemVO>, } as VxeTableGridOptions<ImManagerFaceUserItemApi.FaceUserItem>,
}); });
</script> </script>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ImManagerFriendVO } from '#/api/im/manager/friend'; import type { ImManagerFriendApi } from '#/api/im/manager/friend';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -17,7 +17,7 @@ defineOptions({ name: 'ImManagerFriend' });
const router = useRouter(); const router = useRouter();
/** 查看私聊消息 */ /** 查看私聊消息 */
function handleConversation(row: ImManagerFriendVO) { function handleConversation(row: ImManagerFriendApi.Friend) {
router.push({ router.push({
name: 'ImPrivateMessage', name: 'ImPrivateMessage',
query: { query: {
@ -54,7 +54,7 @@ const [Grid] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<ImManagerFriendVO>, } as VxeTableGridOptions<ImManagerFriendApi.Friend>,
}); });
</script> </script>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ImManagerFriendRequestVO } from '#/api/im/manager/friend/request'; import type { ImManagerFriendRequestApi } from '#/api/im/manager/friend/request';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
@ -42,7 +42,7 @@ const [Grid] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<ImManagerFriendRequestVO>, } as VxeTableGridOptions<ImManagerFriendRequestApi.FriendRequest>,
}); });
</script> </script>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ImManagerGroupVO } from '#/api/im/manager/group'; import type { ImManagerGroupApi } from '#/api/im/manager/group';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -39,12 +39,12 @@ function handleRefresh() {
} }
/** 打开详情 */ /** 打开详情 */
function handleDetail(row: ImManagerGroupVO) { function handleDetail(row: ImManagerGroupApi.Group) {
detailRef.value?.open(row); detailRef.value?.open(row);
} }
/** 查看群聊消息 */ /** 查看群聊消息 */
function handleConversation(row: ImManagerGroupVO) { function handleConversation(row: ImManagerGroupApi.Group) {
router.push({ router.push({
name: 'ImGroupMessage', name: 'ImGroupMessage',
query: { groupId: row.id }, query: { groupId: row.id },
@ -52,12 +52,12 @@ function handleConversation(row: ImManagerGroupVO) {
} }
/** 打开封禁弹窗 */ /** 打开封禁弹窗 */
function handleBan(row: ImManagerGroupVO) { function handleBan(row: ImManagerGroupApi.Group) {
banModalApi.setData(row).open(); banModalApi.setData(row).open();
} }
/** 解封群 */ /** 解封群 */
async function handleUnban(row: ImManagerGroupVO) { async function handleUnban(row: ImManagerGroupApi.Group) {
await confirm(`确认解封群「${row.name}」吗?`); await confirm(`确认解封群「${row.name}」吗?`);
await unbanManagerGroup(row.id); await unbanManagerGroup(row.id);
message.success('解封成功'); message.success('解封成功');
@ -65,7 +65,7 @@ async function handleUnban(row: ImManagerGroupVO) {
} }
/** 解散群 */ /** 解散群 */
async function handleDissolve(row: ImManagerGroupVO) { async function handleDissolve(row: ImManagerGroupApi.Group) {
await confirm(`确认解散群「${row.name}」吗?`); await confirm(`确认解散群「${row.name}」吗?`);
await dissolveManagerGroup(row.id); await dissolveManagerGroup(row.id);
message.success('解散成功'); message.success('解散成功');
@ -99,7 +99,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions<ImManagerGroupVO>, } as VxeTableGridOptions<ImManagerGroupApi.Group>,
}); });
</script> </script>

Some files were not shown because too many files have changed in this diff Show More