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-signature": "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 interface ImChannelMaterialRespVO {
id: number
channelId: number
type: number
title: string
coverUrl?: string
summary?: string
content?: string
url?: string
export namespace ImChannelMaterialApi {
/** 用户端能看到的频道素材详情 */
export interface Material {
id: number;
channelId: number;
type: number;
title: string;
coverUrl?: string;
summary?: 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 interface ImConversationReadRespVO {
id: number // 读位置编号(增量拉取游标用)
conversationType: number // 会话类型,参见 ImConversationType
targetId: number // 会话目标编号
messageId: number // 最大已读消息编号
updateTime?: number // 最近更新时间(毫秒时间戳,增量拉取游标用)
export namespace ImConversationReadApi {
/** IM 会话读位置 Response VO */
export interface ConversationReadRespVO {
id: number; // 读位置编号(增量拉取游标用)
conversationType: number; // 会话类型,参见 ImConversationType
targetId: number; // 会话目标编号
messageId: number; // 最大已读消息编号
updateTime?: number; // 最近更新时间(毫秒时间戳,增量拉取游标用)
}
}
// 增量拉取当前用户的会话读位置(重连 / 离线补偿)
export const pullMyConversationReadList = (params: {
lastId?: number
lastUpdateTime?: number
limit: number
}) => {
return requestClient.get<ImConversationReadRespVO[]>('/im/conversation-read/pull', { params })
/** 增量拉取当前用户的会话读位置(重连 / 离线补偿) */
export function pullMyConversationReadList(params: {
lastId?: number;
lastUpdateTime?: number;
limit: number;
}) {
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 interface ImFacePackUserItemVO {
id: number
url: string
name?: string
width: number
height: number
export namespace ImFacePackApi {
/** 用户端表情包项(精简版) */
export interface FacePackUserItem {
id: number;
url: string;
name?: string;
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 = () => {
return requestClient.get<ImFacePackUserVO[]>('/im/face-pack/list')
/** 拉取所有启用的系统表情包(含表情列表) */
export function getFacePackList() {
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 interface ImFaceUserItemVO {
id: number
url: string
name?: string
width: number
height: number
export namespace ImFaceUserItemApi {
/** 个人表情 */
export interface FaceUserItem {
id: number;
url: string;
name?: string;
width: number;
height: number;
}
/** 添加个人表情请求 */
export interface FaceUserItemSaveReqVO {
url: string;
name?: string;
width: number;
height: number;
}
}
// 添加个人表情请求
export interface ImFaceUserItemSaveReqVO {
url: string
name?: string
width: number
height: number
/** 获取我的个人表情列表 */
export function getFaceUserItemList() {
return requestClient.get<ImFaceUserItemApi.FaceUserItem[]>('/im/face-user-item/list');
}
// 获取我的个人表情列表
export const getFaceUserItemList = () => {
return requestClient.get<ImFaceUserItemVO[]>('/im/face-user-item/list')
/** 添加个人表情 */
export function createFaceUserItem(data: ImFaceUserItemApi.FaceUserItemSaveReqVO) {
return requestClient.post<number>('/im/face-user-item/create', data);
}
// 添加个人表情
export const createFaceUserItem = (data: ImFaceUserItemSaveReqVO) => {
return requestClient.post<number>('/im/face-user-item/create', data)
}
// 删除个人表情
export const deleteFaceUserItem = (id: number) => {
return requestClient.delete('/im/face-user-item/delete', { params: { id } })
/** 删除个人表情 */
export function deleteFaceUserItem(id: number) {
return requestClient.delete<boolean>('/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 interface ImFriendRespVO {
id: number // 关系记录编号
friendUserId: number // 好友的用户编号
silent?: boolean // 是否免打扰
displayName?: string // 好友展示备注(仅自己可见)
displayNamePinyin?: string // 备注的拼音(小写无空格,前端按首字母分桶 / 拼音搜索)
addSource?: number // 添加来源;参见 ImFriendAddSourceEnum
pinned?: boolean // 是否置顶联系人
blocked?: boolean // 是否拉黑
status?: number // 好友状态0=正常1=已删除)
addTime?: string // 添加好友时间
deleteTime?: string // 删除好友时间
updateTime?: number // 最近更新时间(毫秒时间戳,增量拉取游标用)
// 聚合字段(自 AdminUser
nickname?: string // 好友昵称
nicknamePinyin?: string // 昵称的拼音(小写无空格,前端按首字母分桶 / 拼音搜索)
avatar?: string // 好友头像
export namespace ImFriendApi {
/** IM 好友 Response VO */
export interface FriendRespVO {
id: number; // 关系记录编号
friendUserId: number; // 好友的用户编号
silent?: boolean; // 是否免打扰
displayName?: string; // 好友展示备注(仅自己可见)
displayNamePinyin?: string; // 备注的拼音(小写无空格,前端按首字母分桶 / 拼音搜索)
addSource?: number; // 添加来源;参见 ImFriendAddSourceEnum
pinned?: boolean; // 是否置顶联系人
blocked?: boolean; // 是否拉黑
status?: number; // 好友状态0=正常1=已删除)
addTime?: string; // 添加好友时间
deleteTime?: string; // 删除好友时间
updateTime?: number; // 最近更新时间(毫秒时间戳,增量拉取游标用)
nickname?: string; // 好友昵称
nicknamePinyin?: 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 // 好友的用户编号
silent?: boolean // 是否免打扰
displayName?: string // 好友展示备注
pinned?: boolean // 是否置顶联系人
/** 获得当前登录用户的好友列表 */
export function getMyFriendList() {
return requestClient.get<ImFriendApi.FriendRespVO[]>('/im/friend/list');
}
// 获得当前登录用户的好友列表
export const getMyFriendList = () => {
return requestClient.get<ImFriendRespVO[]>('/im/friend/list')
/** 增量拉取当前用户的好友关系(重连 / 离线补偿) */
export function pullMyFriendList(params: {
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 }) => {
return requestClient.get<ImFriendRespVO[]>('/im/friend/pull', { params })
/** 获得好友详情 */
export function getFriend(friendUserId: number | string) {
return requestClient.get<ImFriendApi.FriendRespVO>('/im/friend/get', {
params: { friendUserId },
});
}
// 获得好友详情
export const getFriend = (friendUserId: number | string) => {
return requestClient.get<ImFriendRespVO>('/im/friend/get', { params: { friendUserId } })
/** 删除好友(单向软删除) */
export function deleteFriend(friendUserId: number | string, clear: boolean) {
return requestClient.delete<boolean>('/im/friend/delete', {
params: { friendUserId, clear },
});
}
// 删除好友(单向软删除)
export const deleteFriend = (friendUserId: number | string, clear: boolean) => {
return requestClient.delete<boolean>('/im/friend/delete', { params: { friendUserId, clear } })
/** 更新好友信息(备注 / 免打扰 / 联系人置顶) */
export function updateFriend(data: ImFriendApi.FriendUpdateReqVO) {
return requestClient.put<boolean>('/im/friend/update', data);
}
// 更新好友信息(备注 / 免打扰 / 联系人置顶)
export const updateFriend = (data: ImFriendUpdateReqVO) => {
return requestClient.put<boolean>('/im/friend/update', data)
/** 拉黑好友(必须先是好友;单边屏蔽对方私聊消息) */
export function blockFriend(friendUserId: number | string) {
return requestClient.put<boolean>('/im/friend/block', undefined, {
params: { friendUserId },
});
}
// 拉黑好友(必须先是好友;单边屏蔽对方私聊消息)
export const blockFriend = (friendUserId: number | string) => {
return requestClient.put<boolean>('/im/friend/block', undefined, { params: { friendUserId } })
/** 移出黑名单 */
export function unblockFriend(friendUserId: number | string) {
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 interface ImFriendRequestRespVO {
id: number // 申请编号
fromUserId: number // 发起方用户编号
toUserId: number // 接收方用户编号
handleResult: number // 处理结果0=未处理1=同意2=拒绝
applyContent?: string // 申请理由
handleContent?: string // 处理理由(接收方拒绝时可选填)
addSource?: number // 添加来源;参见 ImFriendAddSourceEnum
handleTime?: string // 处理时间
createTime: string // 申请创建时间
updateTime?: number // 最近更新时间(毫秒时间戳,增量拉取游标用)
// 聚合字段(自 AdminUser
fromNickname?: string // 发起方昵称
fromAvatar?: string // 发起方头像
toNickname?: string // 接收方昵称
toAvatar?: string // 接收方头像
}
// IM 好友申请发起 Request VO
export interface ImFriendRequestApplyReqVO {
toUserId: number // 接收方用户编号
applyContent?: string // 申请理由
displayName?: string // 对接收方的备注(仅自己可见)
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
export namespace ImFriendRequestApi {
/** IM 好友申请 Response VO */
export interface FriendRequestRespVO {
id: number; // 申请编号
fromUserId: number; // 发起方用户编号
toUserId: number; // 接收方用户编号
handleResult: number; // 处理结果0=未处理1=同意2=拒绝
applyContent?: string; // 申请理由
handleContent?: string; // 处理理由(接收方拒绝时可选填)
addSource?: number; // 添加来源;参见 ImFriendAddSourceEnum
handleTime?: string; // 处理时间
createTime: string; // 申请创建时间
updateTime?: number; // 最近更新时间(毫秒时间戳,增量拉取游标用)
fromNickname?: string; // 发起方昵称
fromAvatar?: string; // 发起方头像
toNickname?: string; // 接收方昵称
toAvatar?: string; // 接收方头像
}
/** IM 好友申请发起 Request VO */
export interface FriendRequestApplyReqVO {
toUserId: number; // 接收方用户编号
applyContent?: string; // 申请理由
displayName?: string; // 对接收方的备注(仅自己可见)
addSource?: number; // 添加来源
}
return requestClient.get<ImFriendRequestRespVO[]>('/im/friend-request/list', { params })
}
// 增量拉取「我相关」的好友申请变更(重连 / 离线补偿)
export const pullMyFriendRequestList = (params: {
lastId?: number
lastUpdateTime?: number
limit: number
}) => {
return requestClient.get<ImFriendRequestRespVO[]>('/im/friend-request/pull', { params })
/** 发起好友申请 */
export function applyFriendRequest(data: ImFriendRequestApi.FriendRequestApplyReqVO) {
return requestClient.post<null | number>('/im/friend-request/apply', data);
}
// 按 id 单查「我相关」的申请记录带越权过滤WebSocket 通知到达后用)
export const getMyFriendRequest = (id: number) => {
return requestClient.get<ImFriendRequestRespVO | null>('/im/friend-request/get', { params: { id } })
/** 同意好友申请 */
export function agreeFriendRequest(id: number | string) {
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 interface ImGroupRespVO {
id: number // 编号
name: string // 群名称
ownerUserId: number // 群主用户编号
avatar?: string // 群头像
notice?: string // 群公告
banned?: boolean // 是否封禁
mutedAll?: boolean // 是否全群禁言
joinApproval?: boolean // 进群是否需群主 / 管理员审批
bannedTime?: string // 封禁时间
status: number // 群状态0=正常1=已解散)
dissolvedTime?: string // 解散时间
createTime?: string // 创建时间
pinnedMessages?: ImGroupMessageRespVO[] // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空)
joinStatus?: number // 当前登录用户在该群的成员状态(参见 CommonStatusEnum0 在群 / 1 已退群);历史退群群仍返回,供展示离线消息的群名 / 头像
export namespace ImGroupApi {
/** 群 Response VO */
export interface GroupRespVO {
id: number; // 编号
name: string; // 群名称
ownerUserId: number; // 群主用户编号
avatar?: string; // 群头像
notice?: string; // 群公告
banned?: boolean; // 是否封禁
mutedAll?: boolean; // 是否全群禁言
joinApproval?: boolean; // 进群是否需群主 / 管理员审批
bannedTime?: string; // 封禁时间
status: number; // 群状态0=正常1=已解散)
dissolvedTime?: string; // 解散时间
createTime?: string; // 创建时间
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 // 群编号
messageId: number // 消息编号
/** 获得当前登录用户的群列表 */
export function getMyGroupList() {
return requestClient.get<ImGroupApi.GroupRespVO[]>('/im/group/list');
}
// 群创建 Request VO
export interface ImGroupCreateReqVO {
name: string // 群名称
memberUserIds?: number[] // 初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)
joinApproval?: boolean // 进群是否需审批;不传默认 false 自由进群
/** 获得群详情 */
export function getGroup(id: number | string) {
return requestClient.get<ImGroupApi.GroupRespVO>('/im/group/get', { params: { id } });
}
// 群更新 Request VO
export interface ImGroupUpdateReqVO {
id: number // 群编号
name?: string // 群名称
avatar?: string // 群头像
notice?: string // 群公告
joinApproval?: boolean // 进群是否需审批
/** 创建群 */
export function createGroup(data: ImGroupApi.GroupCreateReqVO) {
return requestClient.post<ImGroupApi.GroupRespVO>('/im/group/create', data);
}
// 添加 / 撤销群管理员 Request VO
export interface ImGroupAdminReqVO {
id: number // 群编号
userIds: number[] // 目标用户编号列表
/** 更新群 */
export function updateGroup(data: ImGroupApi.GroupUpdateReqVO) {
return requestClient.put<ImGroupApi.GroupRespVO>('/im/group/update', data);
}
// 群主转让 Request VO
export interface ImGroupTransferOwnerReqVO {
id: number // 群编号
newOwnerUserId: number // 新群主用户编号
/** 解散群 */
export function dissolveGroup(id: number | string) {
return requestClient.delete<boolean>('/im/group/dissolve', {
params: { id },
});
}
// 全群禁言 / 取消 Request VO
export interface ImGroupMuteAllReqVO {
id: number // 群编号
mutedAll: boolean // 是否全群禁言
/** 添加群管理员(仅群主可调) */
export function addGroupAdmin(data: ImGroupApi.GroupAdminReqVO) {
return requestClient.put<boolean>('/im/group/add-admin', data);
}
// 成员禁言 Request VO
export interface ImGroupMuteMemberReqVO {
id: number // 群编号
userId: number // 被禁言的用户编号
mutedSeconds: number // 禁言时长0 表示永久禁言
/** 撤销群管理员(仅群主可调) */
export function removeGroupAdmin(data: ImGroupApi.GroupAdminReqVO) {
return requestClient.put<boolean>('/im/group/remove-admin', data);
}
// 取消成员禁言 Request VO
export interface ImGroupCancelMuteMemberReqVO {
id: number // 群编号
userId: number // 被取消禁言的用户编号
/** 转让群主(仅老群主可调;旧群主转让后降为普通成员) */
export function transferGroupOwner(data: ImGroupApi.GroupTransferOwnerReqVO) {
return requestClient.put<boolean>('/im/group/transfer-owner', data);
}
// 获得当前登录用户的群列表
export const getMyGroupList = () => {
return requestClient.get<ImGroupRespVO[]>('/im/group/list')
/** 置顶群消息(仅群主 / 管理员可调) */
export function pinGroupMessage(data: ImGroupApi.GroupMessagePinReqVO) {
return requestClient.put<boolean>('/im/group/pin-message', data);
}
// 获得群详情
export const getGroup = (id: number | string) => {
return requestClient.get<ImGroupRespVO>('/im/group/get', { params: { id } })
/** 取消置顶群消息(仅群主 / 管理员可调) */
export function unpinGroupMessage(data: ImGroupApi.GroupMessagePinReqVO) {
return requestClient.put<boolean>('/im/group/unpin-message', data);
}
// 创建群
export const createGroup = (data: ImGroupCreateReqVO) => {
return requestClient.post<ImGroupRespVO>('/im/group/create', data)
/** 全群禁言 / 取消(仅群主 / 管理员可调) */
export function muteAll(data: ImGroupApi.GroupMuteAllReqVO) {
return requestClient.put<boolean>('/im/group/mute-all', data);
}
// 更新群
export const updateGroup = (data: ImGroupUpdateReqVO) => {
return requestClient.put<ImGroupRespVO>('/im/group/update', data)
/** 禁言成员 */
export function muteMember(data: ImGroupApi.GroupMuteMemberReqVO) {
return requestClient.put<boolean>('/im/group/mute-member', data);
}
// 解散群
export const dissolveGroup = (id: number | string) => {
return requestClient.delete<boolean>('/im/group/dissolve', { params: { id } })
}
// 添加群管理员(仅群主可调)
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)
/** 取消成员禁言 */
export function cancelMuteMember(data: ImGroupApi.GroupCancelMuteMemberReqVO) {
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 interface ImGroupMemberRespVO {
id: number // 编号
groupId: number // 群编号
userId: number // 用户编号
displayUserName?: string // 组内显示名(群主设置的备注)
groupRemark?: string // 群备注(当前用户对群的备注)
silent?: boolean // 是否免打扰
status?: number // 成员状态0=在群1=退群)
role?: number // 成员角色,参见 ImGroupMemberRole 枚举
joinTime?: string // 入群时间
quitTime?: string // 退群时间
muteEndTime?: string // 禁言到期时间
createTime?: string // 创建时间
// 聚合字段(自 AdminUser
nickname?: string // 用户昵称
avatar?: string // 用户头像
export namespace ImGroupMemberApi {
/** 群成员 Response VO */
export interface GroupMemberRespVO {
id: number; // 编号
groupId: number; // 群编号
userId: number; // 用户编号
displayUserName?: string; // 组内显示名(群主设置的备注)
groupRemark?: string; // 群备注(当前用户对群的备注)
silent?: boolean; // 是否免打扰
status?: number; // 成员状态0=在群1=退群)
role?: number; // 成员角色,参见 ImGroupMemberRole 枚举
joinTime?: string; // 入群时间
quitTime?: string; // 退群时间
muteEndTime?: string; // 禁言到期时间
createTime?: string; // 创建时间
nickname?: 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 // 群编号
memberUserIds: number[] // 被邀请的用户编号列表
/** 邀请用户加入群 */
export function inviteGroupMember(data: ImGroupMemberApi.GroupMemberInviteReqVO) {
return requestClient.post<boolean>('/im/group/invite', data);
}
// 群成员移除 Request VO
export interface ImGroupMemberRemoveReqVO {
groupId: number // 群编号
memberUserIds: number[] // 被移除的用户编号列表
/** 退出群 */
export function quitGroup(groupId: number | string) {
return requestClient.delete<boolean>('/im/group/quit', {
params: { groupId },
});
}
// 群成员更新 Request VO
export interface ImGroupMemberUpdateReqVO {
groupId: number // 群编号
displayUserName?: string // 群内昵称
groupRemark?: string // 群备注
silent?: boolean // 是否免打扰
/** 移除群成员 */
export function removeGroupMember(data: ImGroupMemberApi.GroupMemberRemoveReqVO) {
return requestClient.delete<boolean>('/im/group/kicking', { data });
}
// 邀请用户加入群
export const inviteGroupMember = (data: ImGroupMemberInviteReqVO) => {
return requestClient.post<boolean>('/im/group/invite', data)
/** 获得群成员详情 */
export function getGroupMember(groupId: number, userId: number) {
return requestClient.get<ImGroupMemberApi.GroupMemberRespVO>('/im/group-member/get', {
params: { groupId, userId },
});
}
// 退出群
export const quitGroup = (groupId: number | string) => {
return requestClient.delete<boolean>('/im/group/quit', { params: { groupId } })
/** 获得指定群的成员列表(聚合 AdminUser 昵称 / 头像) */
export function getGroupMemberList(groupId: number | string) {
return requestClient.get<ImGroupMemberApi.GroupMemberRespVO[]>('/im/group-member/list', {
params: { groupId },
});
}
// 移除群成员
export const removeGroupMember = (data: ImGroupMemberRemoveReqVO) => {
return requestClient.delete<boolean>('/im/group/kicking', { 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)
/** 更新群成员 */
export function updateGroupMember(data: ImGroupMemberApi.GroupMemberUpdateReqVO) {
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 interface ImGroupRequestRespVO {
id: number // 申请编号
groupId: number // 群编号
userId: number // 申请人 / 被邀请人用户编号
inviterUserId?: number // 邀请人NULL 表示用户主动申请
handleResult: number // 处理结果0=未处理1=同意2=拒绝
applyContent?: string // 申请理由
handleContent?: string // 处理理由(拒绝时可选填)
handleUserId?: number // 处理人用户编号
addSource?: number // 加入来源;参见 ImGroupAddSourceEnum
handleTime?: string // 处理时间
createTime: string // 申请创建时间
updateTime?: number // 最近更新时间(毫秒时间戳,增量拉取游标用)
// 聚合字段
userNickname?: string // 申请人 / 被邀请人昵称
userAvatar?: string // 申请人 / 被邀请人头像
inviterNickname?: string // 邀请人昵称
inviterAvatar?: string // 邀请人头像
groupName?: string // 群名称
groupAvatar?: string // 群头像
export namespace ImGroupRequestApi {
/** IM 加群申请 Response VO */
export interface GroupRequestRespVO {
id: number; // 申请编号
groupId: number; // 群编号
userId: number; // 申请人 / 被邀请人用户编号
inviterUserId?: number; // 邀请人NULL 表示用户主动申请
handleResult: number; // 处理结果0=未处理1=同意2=拒绝
applyContent?: string; // 申请理由
handleContent?: string; // 处理理由(拒绝时可选填)
handleUserId?: number; // 处理人用户编号
addSource?: number; // 加入来源;参见 ImGroupAddSourceEnum
handleTime?: string; // 处理时间
createTime: string; // 申请创建时间
updateTime?: number; // 最近更新时间(毫秒时间戳,增量拉取游标用)
userNickname?: string; // 申请人 / 被邀请人昵称
userAvatar?: string; // 申请人 / 被邀请人头像
inviterNickname?: string; // 邀请人昵称
inviterAvatar?: string; // 邀请人头像
groupName?: string; // 群名称
groupAvatar?: string; // 群头像
}
/** IM 加群申请发起 Request VO */
export interface GroupRequestApplyReqVO {
groupId: number; // 群编号
applyContent?: string; // 申请理由
addSource?: number; // 加入来源
}
}
// IM 加群申请发起 Request VO
export interface ImGroupRequestApplyReqVO {
groupId: number // 群编号
applyContent?: string // 申请理由
addSource?: number // 加入来源
/** 申请加群 */
export function applyJoinGroup(data: ImGroupRequestApi.GroupRequestApplyReqVO) {
return requestClient.post<null | number>('/im/group-request/apply', data);
}
// 申请加群
export const applyJoinGroup = (data: ImGroupRequestApplyReqVO) => {
return requestClient.post<null | number>('/im/group-request/apply', data)
/** 同意加群申请(群主或管理员) */
export function agreeGroupRequest(id: number | string) {
return requestClient.put<boolean>('/im/group-request/agree', undefined, {
params: { id },
});
}
// 同意加群申请(群主或管理员)
export const agreeGroupRequest = (id: number | string) => {
return requestClient.put<boolean>('/im/group-request/agree', undefined, { params: { id } })
/** 拒绝加群申请(群主或管理员) */
export function refuseGroupRequest(
id: number | string,
handleContent?: string,
) {
return requestClient.put<boolean>('/im/group-request/refuse', undefined, {
params: { id, handleContent },
});
}
// 拒绝加群申请(群主或管理员)
export const refuseGroupRequest = (id: number | string, handleContent?: string) => {
return requestClient.put<boolean>('/im/group-request/refuse', undefined, { params: { id, handleContent } })
/** 查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表 */
export function getUnhandledRequestList() {
return requestClient.get<ImGroupRequestApi.GroupRequestRespVO[]>(
'/im/group-request/unhandled-list',
);
}
// 查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表
export const getUnhandledRequestList = () => {
return requestClient.get<ImGroupRequestRespVO[]>('/im/group-request/unhandled-list')
/** 查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查 */
export function getGroupRequestListByGroupId(groupId: number) {
return requestClient.get<ImGroupRequestApi.GroupRequestRespVO[]>(
'/im/group-request/list-by-group',
{ params: { groupId } },
);
}
// 查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查
export const getGroupRequestListByGroupId = (groupId: number) => {
return requestClient.get<ImGroupRequestRespVO[]>('/im/group-request/list-by-group', { params: { groupId } })
/** 按 id 单查申请记录带越权过滤WebSocket 通知到达后用) */
export function getMyGroupRequest(id: number) {
return requestClient.get<ImGroupRequestApi.GroupRequestRespVO | null>(
'/im/group-request/get',
{ params: { id } },
);
}
// 按 id 单查申请记录带越权过滤WebSocket 通知到达后用)
export const getMyGroupRequest = (id: number) => {
return requestClient.get<ImGroupRequestRespVO | null>('/im/group-request/get', { params: { id } })
}
// 增量拉取我管理的所有群下加群申请变更(重连 / 离线补偿)
export const pullMyGroupRequestList = (params: { lastId?: number; lastUpdateTime?: number; limit: number }) => {
return requestClient.get<ImGroupRequestRespVO[]>('/im/group-request/pull', { params })
/** 增量拉取我管理的所有群下加群申请变更(重连 / 离线补偿) */
export function pullMyGroupRequestList(params: {
lastId?: number;
lastUpdateTime?: number;
limit: number;
}) {
return requestClient.get<ImGroupRequestApi.GroupRequestRespVO[]>(
'/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 {
id: number
code: string
name: string
avatar?: string
sort: number
status: number
createTime?: Date
export namespace ImManagerChannelApi {
/** 频道 */
export interface Channel {
id: number;
code: string;
name: string;
avatar?: string;
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) => {
return requestClient.get('/im/manager/channel/get', { params: { id } })
/** 获得频道详情 */
export function getManagerChannel(id: number) {
return requestClient.get<ImManagerChannelApi.Channel>('/im/manager/channel/get', {
params: { id },
});
}
// 新增频道
export const createManagerChannel = (data: ImManagerChannelVO) => {
return requestClient.post('/im/manager/channel/create', data)
/** 新增频道 */
export function createManagerChannel(data: ImManagerChannelApi.Channel) {
return requestClient.post<number>('/im/manager/channel/create', data);
}
// 修改频道
export const updateManagerChannel = (data: ImManagerChannelVO) => {
return requestClient.put('/im/manager/channel/update', data)
/** 修改频道 */
export function updateManagerChannel(data: ImManagerChannelApi.Channel) {
return requestClient.put<boolean>('/im/manager/channel/update', data);
}
// 删除频道
export const deleteManagerChannel = (id: number) => {
return requestClient.delete('/im/manager/channel/delete', { params: { id } })
/** 删除频道 */
export function deleteManagerChannel(id: number) {
return requestClient.delete<boolean>('/im/manager/channel/delete', {
params: { id },
});
}
// 获得启用的频道精简列表(表单选择用)
export const getSimpleChannelList = () => {
return requestClient.get<ImManagerChannelVO[]>('/im/manager/channel/simple-list')
/** 获得启用的频道精简列表(表单选择用) */
export function getSimpleChannelList() {
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 {
id: number
channelId: number
channelName?: string
type: number
title: string
coverUrl?: string
summary?: string
content?: string
url?: string
createTime?: Date
export namespace ImManagerChannelMaterialApi {
/** 频道素材 */
export interface Material {
id: number;
channelId: number;
channelName?: string;
type: number;
title: string;
coverUrl?: string;
summary?: string;
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) => {
return requestClient.get('/im/manager/channel-material/simple-list', { params: { channelId } })
/** 获得指定频道下的素材精简列表 */
export function getSimpleManagerChannelMaterialList(channelId: number) {
return requestClient.get<ImManagerChannelMaterialApi.Material[]>(
'/im/manager/channel-material/simple-list',
{ params: { channelId } },
);
}
// 获得素材详情
export const getManagerChannelMaterial = (id: number) => {
return requestClient.get('/im/manager/channel-material/get', { params: { id } })
/** 获得素材详情 */
export function getManagerChannelMaterial(id: number) {
return requestClient.get<ImManagerChannelMaterialApi.Material>(
'/im/manager/channel-material/get',
{ params: { id } },
);
}
// 新增素材
export const createManagerChannelMaterial = (data: ImManagerChannelMaterialVO) => {
return requestClient.post('/im/manager/channel-material/create', data)
/** 新增素材 */
export function createManagerChannelMaterial(
data: ImManagerChannelMaterialApi.Material,
) {
return requestClient.post<number>('/im/manager/channel-material/create', data);
}
// 修改素材
export const updateManagerChannelMaterial = (data: ImManagerChannelMaterialVO) => {
return requestClient.put('/im/manager/channel-material/update', data)
/** 修改素材 */
export function updateManagerChannelMaterial(
data: ImManagerChannelMaterialApi.Material,
) {
return requestClient.put<boolean>(
'/im/manager/channel-material/update',
data,
);
}
// 删除素材
export const deleteManagerChannelMaterial = (id: number) => {
return requestClient.delete('/im/manager/channel-material/delete', { params: { id } })
/** 删除素材 */
export function deleteManagerChannelMaterial(id: number) {
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 {
id: number
channelId: number
channelName?: string
materialId: number
materialTitle?: string
materialCoverUrl?: string
type: number
content?: string
receiverUserIds?: number[]
sendTime?: Date
export namespace ImManagerChannelMessageApi {
/** 频道消息 */
export interface ChannelMessage {
id: number;
channelId: number;
channelName?: string;
materialId: number;
materialTitle?: string;
materialCoverUrl?: string;
type: number;
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) => {
return requestClient.post('/im/manager/channel-message/send', data)
/** 删除频道消息 */
export function deleteManagerChannelMessage(id: number) {
return requestClient.delete<boolean>('/im/manager/channel-message/delete', {
params: { id },
});
}
// 删除频道消息
export const deleteManagerChannelMessage = (id: number) => {
return requestClient.delete('/im/manager/channel-message/delete', { params: { id } })
}
// 获得频道消息分页
export const getManagerChannelMessagePage = (params: PageParam) => {
return requestClient.get('/im/manager/channel-message/page', { params })
/** 获得频道消息分页 */
export function getManagerChannelMessagePage(params: PageParam) {
return requestClient.get<PageResult<ImManagerChannelMessageApi.ChannelMessage>>(
'/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 {
id: number
packId: number
url: string
name?: string
width: number
height: number
sort: number
status: number
createTime?: Date
export namespace ImManagerFacePackItemApi {
/** 表情项 */
export interface FacePackItem {
id: number;
packId: number;
url: string;
name?: string;
width: number;
height: number;
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) => {
return requestClient.get('/im/manager/face-pack-item/get', { params: { id } })
/** 获得表情详情 */
export function getManagerFacePackItem(id: number) {
return requestClient.get<ImManagerFacePackItemApi.FacePackItem>(
'/im/manager/face-pack-item/get',
{ params: { id } },
);
}
// 新增表情
export const createManagerFacePackItem = (data: ImManagerFacePackItemVO) => {
return requestClient.post('/im/manager/face-pack-item/create', data)
/** 新增表情 */
export function createManagerFacePackItem(data: ImManagerFacePackItemApi.FacePackItem) {
return requestClient.post<number>('/im/manager/face-pack-item/create', data);
}
// 修改表情
export const updateManagerFacePackItem = (data: ImManagerFacePackItemVO) => {
return requestClient.put('/im/manager/face-pack-item/update', data)
/** 修改表情 */
export function updateManagerFacePackItem(data: ImManagerFacePackItemApi.FacePackItem) {
return requestClient.put<boolean>(
'/im/manager/face-pack-item/update',
data,
);
}
// 删除表情
export const deleteManagerFacePackItem = (id: number) => {
return requestClient.delete('/im/manager/face-pack-item/delete', { params: { id } })
/** 删除表情 */
export function deleteManagerFacePackItem(id: number) {
return requestClient.delete<boolean>('/im/manager/face-pack-item/delete', {
params: { id },
});
}
// 批量删除表情
export const deleteManagerFacePackItemList = (ids: number[]) => {
return requestClient.delete('/im/manager/face-pack-item/delete-list', {
params: { ids: ids.join(',') }
})
/** 批量删除表情 */
export function deleteManagerFacePackItemList(ids: number[]) {
return requestClient.delete<boolean>(
'/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 {
id: number
name: string
icon?: string
sort: number
status: number
createTime?: Date
export namespace ImManagerFacePackApi {
/** 表情包 */
export interface FacePack {
id: number;
name: string;
icon?: string;
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) => {
return requestClient.get('/im/manager/face-pack/get', { params: { id } })
/** 获得表情包详情 */
export function getManagerFacePack(id: number) {
return requestClient.get<ImManagerFacePackApi.FacePack>('/im/manager/face-pack/get', {
params: { id },
});
}
// 新增表情包
export const createManagerFacePack = (data: ImManagerFacePackVO) => {
return requestClient.post('/im/manager/face-pack/create', data)
/** 新增表情包 */
export function createManagerFacePack(data: ImManagerFacePackApi.FacePack) {
return requestClient.post<number>('/im/manager/face-pack/create', data);
}
// 修改表情包
export const updateManagerFacePack = (data: ImManagerFacePackVO) => {
return requestClient.put('/im/manager/face-pack/update', data)
/** 修改表情包 */
export function updateManagerFacePack(data: ImManagerFacePackApi.FacePack) {
return requestClient.put<boolean>('/im/manager/face-pack/update', data);
}
// 删除表情包
export const deleteManagerFacePack = (id: number) => {
return requestClient.delete('/im/manager/face-pack/delete', { params: { id } })
/** 删除表情包 */
export function deleteManagerFacePack(id: number) {
return requestClient.delete<boolean>('/im/manager/face-pack/delete', {
params: { id },
});
}
// 批量删除表情包
export const deleteManagerFacePackList = (ids: number[]) => {
return requestClient.delete('/im/manager/face-pack/delete-list', {
params: { ids: ids.join(',') }
})
/** 批量删除表情包 */
export function deleteManagerFacePackList(ids: number[]) {
return requestClient.delete<boolean>('/im/manager/face-pack/delete-list', {
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 {
id: number
userId: number
userNickname?: string
url: string
name?: string
width?: number
height?: number
createTime?: Date
export namespace ImManagerFaceUserItemApi {
/** 用户表情 */
export interface FaceUserItem {
id: number;
userId: number;
userNickname?: string;
url: string;
name?: string;
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) => {
return requestClient.delete('/im/manager/face-user-item/delete', { params: { id } })
/** 删除用户表情 */
export function deleteManagerFaceUserItem(id: number) {
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 {
id: number
userId: number
userNickname?: string
friendUserId: number
friendNickname?: string
displayName?: string
addSource?: number
silent: boolean
pinned: boolean
blocked: boolean
status: number
addTime?: Date
deleteTime?: Date
createTime: Date
export namespace ImManagerFriendApi {
/** 好友关系 */
export interface Friend {
id: number;
userId: number;
userNickname?: string;
friendUserId: number;
friendNickname?: string;
displayName?: string;
addSource?: number;
silent: boolean;
pinned: boolean;
blocked: boolean;
status: number;
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 {
id: number
fromUserId: number
fromNickname?: string
toUserId: number
toNickname?: string
applyContent?: string
displayName?: string
addSource?: number
handleResult: number
handleContent?: string
handleTime?: Date
createTime: Date
export namespace ImManagerFriendRequestApi {
/** 好友申请 */
export interface FriendRequest {
id: number;
fromUserId: number;
fromNickname?: string;
toUserId: number;
toNickname?: string;
applyContent?: string;
displayName?: string;
addSource?: number;
handleResult: number;
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 {
id: number
name: string
avatar?: string
notice?: string
ownerUserId: number
ownerNickname?: string
memberCount?: number
status: number
banned: boolean
mutedAll?: boolean
bannedReason?: string
bannedTime?: Date
dissolvedTime?: Date
createTime: Date
export namespace ImManagerGroupApi {
/** 群 */
export interface Group {
id: number;
name: string;
avatar?: string;
notice?: string;
ownerUserId: number;
ownerNickname?: string;
memberCount?: number;
status: number;
banned: boolean;
mutedAll?: boolean; // 是否全群禁言
bannedReason?: string;
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
avatar?: string
displayUserName?: string
groupRemark?: string
silent?: boolean
status: number
role?: number
joinTime?: Date
quitTime?: Date
muteEndTime?: Date
/** 获得群分页 */
export function getManagerGroupPage(params: PageParam) {
return requestClient.get<PageResult<ImManagerGroupApi.Group>>(
'/im/manager/group/page',
{ params },
);
}
// 获得群分页
export const getManagerGroupPage = (params: PageParam) => {
return requestClient.get('/im/manager/group/page', { params })
/** 获得群详情 */
export function getManagerGroup(id: number) {
return requestClient.get<ImManagerGroupApi.Group>('/im/manager/group/get', {
params: { id },
});
}
// 获得群详情
export const getManagerGroup = (id: number) => {
return requestClient.get('/im/manager/group/get', { params: { id } })
/** 封禁群 */
export function banManagerGroup(data: ImManagerGroupApi.GroupBanReqVO) {
return requestClient.put<boolean>('/im/manager/group/ban', data);
}
// 封禁群
export const banManagerGroup = (data: { id: number; reason: string }) => {
return requestClient.put('/im/manager/group/ban', data)
/** 解封群 */
export function unbanManagerGroup(id: number) {
return requestClient.put<boolean>('/im/manager/group/unban', undefined, {
params: { id },
});
}
// 解封群
export const unbanManagerGroup = (id: number) => {
return requestClient.put('/im/manager/group/unban', undefined, { params: { id } })
/** 解散群 */
export function dissolveManagerGroup(id: number) {
return requestClient.delete<boolean>('/im/manager/group/dissolve', {
params: { id },
});
}
// 解散群
export const dissolveManagerGroup = (id: number) => {
return requestClient.delete('/im/manager/group/dissolve', { params: { id } })
}
// 获得群成员列表
export const getManagerGroupMemberList = (groupId: number) => {
return requestClient.get('/im/manager/group/member/list', { params: { groupId } })
/** 获得群成员列表(含已退群成员,由前端按需过滤) */
export function getManagerGroupMemberList(groupId: number) {
return requestClient.get<ImManagerGroupApi.GroupMember[]>(
'/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 {
id: number
groupId: number
groupName?: string
userId: number
userNickname?: string
inviterUserId?: number
inviterNickname?: string
applyContent?: string
addSource?: number
handleResult: number
handleUserId?: number
handleNickname?: string
handleContent?: string
handleTime?: Date
createTime: Date
export namespace ImManagerGroupRequestApi {
/** 加群申请 */
export interface GroupRequest {
id: number;
groupId: number;
groupName?: string;
userId: number;
userNickname?: string;
inviterUserId?: number;
inviterNickname?: string;
applyContent?: string;
addSource?: number;
handleResult: number;
handleUserId?: number;
handleNickname?: string;
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 {
id: number
clientMessageId?: string
groupId: number
groupName?: string
senderId: number
senderNickname?: string
type: number
content: string
status: number
atUserIds?: number[]
atUserNicknames?: (null | string)[]
receiptStatus: number
sendTime: Date
createTime: Date
export namespace ImManagerGroupMessageApi {
/** 群聊消息 */
export interface GroupMessage {
id: number;
clientMessageId?: string;
groupId: number;
groupName?: string;
senderId: number;
senderNickname?: string;
type: number;
content: string;
status: number;
atUserIds?: number[];
atUserNicknames?: (null | string)[];
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) => {
return requestClient.get('/im/manager/message/group/get', { params: { id } })
/** 获得群聊消息详情 */
export function getManagerGroupMessage(id: number) {
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 {
id: number
clientMessageId?: string
senderId: number
senderNickname?: string
receiverId: number
receiverNickname?: string
type: number
content: string
status: number
receiptStatus: number
sendTime: Date
createTime: Date
export namespace ImManagerPrivateMessageApi {
/** 私聊消息 */
export interface PrivateMessage {
id: number;
clientMessageId?: string;
senderId: number;
senderNickname?: string;
receiverId: number;
receiverNickname?: string;
type: number;
content: string;
status: number;
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) => {
return requestClient.get('/im/manager/message/private/get', { params: { id } })
/** 获得私聊消息详情 */
export function getManagerPrivateMessage(id: number) {
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 {
id: number
room: string
conversationType: number
mediaType: number
inviterUserId: number
inviterNickname?: string
groupId?: number
groupName?: string
status: number
endReason?: number
startTime: Date
acceptTime?: Date
endTime?: Date
createTime: Date
export namespace ImManagerRtcApi {
/** RTC 通话 */
export interface RtcCall {
id: number;
room: string;
conversationType: number;
mediaType: number;
inviterUserId: number;
inviterNickname?: string;
groupId?: number;
groupName?: string;
status: number;
endReason?: number;
startTime: 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
userId: number
userNickname?: string
role: number
status: number
inviteTime: Date
acceptTime?: Date
leaveTime?: Date
/** 获得通话记录分页 */
export function getManagerRtcCallPage(params: PageParam) {
return requestClient.get<PageResult<ImManagerRtcApi.RtcCall>>(
'/im/manager/rtc/page',
{ params },
);
}
// 获得通话记录分页
export const getManagerRtcCallPage = (params: PageParam) => {
return requestClient.get('/im/manager/rtc/page', { params })
}
// 获得通话参与者列表
export const getManagerRtcCallParticipantList = (id: number) => {
return requestClient.get('/im/manager/rtc/participant-list', { params: { id } })
/** 获得通话参与者列表 */
export function getManagerRtcCallParticipantList(id: number) {
return requestClient.get<ImManagerRtcApi.RtcParticipant[]>(
'/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 {
id: number
word: string
status: number
creator?: string
creatorName?: string
createTime?: Date
export namespace ImManagerSensitiveWordApi {
/** 敏感词 */
export interface SensitiveWord {
id: number;
word: string;
status: number;
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) => {
return requestClient.get('/im/manager/sensitive-word/get', { params: { id } })
/** 获得敏感词详情 */
export function getManagerSensitiveWord(id: number) {
return requestClient.get<ImManagerSensitiveWordApi.SensitiveWord>(
'/im/manager/sensitive-word/get',
{ params: { id } },
);
}
// 新增敏感词
export const createManagerSensitiveWord = (data: ImManagerSensitiveWordVO) => {
return requestClient.post('/im/manager/sensitive-word/create', data)
/** 新增敏感词 */
export function createManagerSensitiveWord(data: ImManagerSensitiveWordApi.SensitiveWord) {
return requestClient.post<number>('/im/manager/sensitive-word/create', data);
}
// 修改敏感词
export const updateManagerSensitiveWord = (data: ImManagerSensitiveWordVO) => {
return requestClient.put('/im/manager/sensitive-word/update', data)
/** 修改敏感词 */
export function updateManagerSensitiveWord(data: ImManagerSensitiveWordApi.SensitiveWord) {
return requestClient.put<boolean>(
'/im/manager/sensitive-word/update',
data,
);
}
// 删除敏感词
export const deleteManagerSensitiveWord = (id: number) => {
return requestClient.delete('/im/manager/sensitive-word/delete', { params: { id } })
/** 删除敏感词 */
export function deleteManagerSensitiveWord(id: number) {
return requestClient.delete<boolean>('/im/manager/sensitive-word/delete', {
params: { id },
});
}
// 批量删除敏感词
export const deleteManagerSensitiveWordList = (ids: number[]) => {
return requestClient.delete('/im/manager/sensitive-word/delete-list', {
params: { ids: ids.join(',') }
})
/** 批量删除敏感词 */
export function deleteManagerSensitiveWordList(ids: number[]) {
return requestClient.delete<boolean>(
'/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 {
totalUser: number
newUserToday: number
totalGroup: number
newGroupToday: number
activeUserDaily: number
activeUserWeekly: number
activeUserMonthly: number
privateMessageToday: number
groupMessageToday: number
privateMessageYesterday: number
groupMessageYesterday: number
export namespace ImManagerStatisticsApi {
/** 统计概览 */
export interface Overview {
totalUser: number;
newUserToday: number;
totalGroup: number;
newGroupToday: number;
activeUserDaily: number;
activeUserWeekly: number;
activeUserMonthly: number;
privateMessageToday: 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[]
series: Record<string, number[]>
/** 获得 KPI 概览 */
export function getStatisticsOverview() {
return requestClient.get<ImManagerStatisticsApi.Overview>(
'/im/manager/statistics/overview',
);
}
export interface ImStatisticsMessageTypeVO {
type: number
value: number
/** 获得消息趋势(私聊 + 群聊双线) */
export function getMessageTrend(days: number) {
return requestClient.get<ImManagerStatisticsApi.Trend>(
'/im/manager/statistics/message-trend',
{ params: { days } },
);
}
export interface ImStatisticsGroupSizeVO {
range: string
count: number
/** 获得用户趋势(新增注册 + 日活双线) */
export function getUserTrend(days: number) {
return requestClient.get<ImManagerStatisticsApi.Trend>(
'/im/manager/statistics/user-trend',
{ params: { days } },
);
}
export interface ImStatisticsTopSenderVO {
userId: number
nickname: string
messageCount: number
/** 获得内容类型分布(最近 30 天) */
export function getMessageTypeDistribution() {
return requestClient.get<ImManagerStatisticsApi.MessageType[]>(
'/im/manager/statistics/message-type-distribution',
);
}
// 获得 KPI 概览
export const getStatisticsOverview = (): Promise<ImStatisticsOverviewVO> => {
return requestClient.get('/im/manager/statistics/overview')
/** 获得群规模分布 */
export function getGroupSizeDistribution() {
return requestClient.get<ImManagerStatisticsApi.GroupSize[]>(
'/im/manager/statistics/group-size-distribution',
);
}
// 获得消息趋势
export const getMessageTrend = (days: number): Promise<ImStatisticsTrendVO> => {
return requestClient.get('/im/manager/statistics/message-trend', { params: { days } })
}
// 获得用户趋势
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')
/** 获得消息 TOP 发送者(最近 30 天) */
export function getTopSenders() {
return requestClient.get<ImManagerStatisticsApi.TopSender[]>(
'/im/manager/statistics/top-senders',
);
}

View File

@ -1,22 +1,34 @@
import { requestClient } from '#/api/request'
import { requestClient } from '#/api/request';
export interface ImChannelMessageRespVO {
id: number
clientMessageId?: string
channelId: number
materialId: number
type: number
content: string
receiptStatus?: number
sendTime: string
export namespace ImChannelMessageApi {
/** 频道消息 */
export interface ChannelMessageRespVO {
id: number;
clientMessageId?: string;
channelId: number;
materialId: number;
type: number;
content: string;
receiptStatus?: number;
sendTime: string;
}
}
// 拉取当前用户应收的频道消息(离线增量);按 minId 游标分页
export const pullChannelMessages = (params: { minId: number; size?: number }, signal?: AbortSignal) => {
return requestClient.get<ImChannelMessageRespVO[]>('/im/channel/message/pull', { params, signal })
/** 拉取当前用户应收的频道消息(离线增量);按 minId 游标分页 */
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) => {
return requestClient.put('/im/channel/message/read', undefined, { params: { channelId, messageId } })
/** 上报频道消息已读位置;切到频道会话或拉到新消息后调 */
export function readChannelMessages(channelId: number, messageId: number) {
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 interface ImGroupMessageRespVO {
id: number // 消息编号
clientMessageId: string // 客户端消息编号
senderId: number // 发送人编号
groupId: number // 群编号
type: number // 内容类型
content: string // 消息内容JSON 格式)
status: number // 消息状态
sendTime: string // 发送时间
atUserIds?: number[] // @ 目标用户编号列表
receiverUserIds?: number[] // 定向接收用户编号列表
receiptStatus?: number // 回执状态
readCount?: number // 已读人数(回执消息、且发送人为当前用户时有值)
export namespace ImGroupMessageApi {
/** 群聊消息 Response VO */
export interface GroupMessageRespVO {
id: number; // 消息编号
clientMessageId: string; // 客户端消息编号
senderId: number; // 发送人编号
groupId: number; // 群编号
type: number; // 内容类型
content: string; // 消息内容JSON 格式)
status: number; // 消息状态
sendTime: string; // 发送时间
atUserIds?: number[]; // @ 目标用户编号列表
receiverUserIds?: 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 // 客户端消息编号
groupId: number // 群编号
type: number // 内容类型
content: string // 消息内容JSON 格式)
atUserIds?: number[] // @ 目标用户编号列表
receipt?: boolean // 是否需要回执
/** 发送群聊消息 */
export function sendGroupMessage(data: ImGroupMessageApi.GroupMessageSendReqVO) {
return requestClient.post<ImGroupMessageApi.GroupMessageRespVO>(
'/im/message/group/send',
data,
);
}
// 群聊历史消息列表 Request VO
export interface ImGroupMessageListReqVO {
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 = (
/** 拉取群聊消息(增量) */
export function pullGroupMessages(
params: { minId: number | string; size: number },
signal?: AbortSignal
) => {
return requestClient.get<ImGroupMessageRespVO[]>('/im/message/group/pull', { params, signal })
signal?: AbortSignal,
) {
return requestClient.get<ImGroupMessageApi.GroupMessageRespVO[]>(
'/im/message/group/pull',
{ params, signal },
);
}
// 查询群聊历史消息
export const getGroupMessageList = (params: ImGroupMessageListReqVO) => {
return requestClient.get<ImGroupMessageRespVO[]>('/im/message/group/list', { params })
/** 查询群聊历史消息 */
export function getGroupMessageList(params: ImGroupMessageApi.GroupMessageListReqVO) {
return requestClient.get<ImGroupMessageApi.GroupMessageRespVO[]>(
'/im/message/group/list',
{ params },
);
}
// 标记群聊消息已读
export const readGroupMessages = (groupId: number | string, messageId: number | string) => {
return requestClient.put<boolean>('/im/message/group/read', undefined, { params: { groupId, messageId } })
/** 标记群聊消息已读 */
export function readGroupMessages(
groupId: number | string,
messageId: number | string,
) {
return requestClient.put<boolean>('/im/message/group/read', undefined, {
params: { groupId, messageId },
});
}
// 撤回群聊消息
export const recallGroupMessage = (id: number | string) => {
return requestClient.delete<ImGroupMessageRespVO>('/im/message/group/recall', { params: { id } })
/** 撤回群聊消息 */
export function recallGroupMessage(id: number | string) {
return requestClient.delete<ImGroupMessageApi.GroupMessageRespVO>(
'/im/message/group/recall',
{ params: { id } },
);
}
// 获取群消息已读用户列表
export const getGroupReadUsers = (params: {
groupId: number | string
messageId: number | string
}) => {
return requestClient.get<number[]>('/im/message/group/get-read-user-ids', { params })
/** 获取群消息已读用户列表 */
export function getGroupReadUsers(params: {
groupId: number | string;
messageId: number | string;
}) {
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 interface ImPrivateMessageRespVO {
id: number // 消息编号
clientMessageId: string // 客户端消息编号
senderId: number // 发送人编号
receiverId: number // 接收人编号
type: number // 内容类型
content: string // 消息内容JSON 格式)
status: number // 消息状态(正常 / 已撤回)
receiptStatus?: number // 回执状态(不需要 / 待完成 / 已完成),对齐 ImMessageReceiptStatus
sendTime: string // 发送时间
export namespace ImPrivateMessageApi {
/** 私聊消息 Response VO */
export interface PrivateMessageRespVO {
id: number; // 消息编号
clientMessageId: string; // 客户端消息编号
senderId: number; // 发送人编号
receiverId: number; // 接收人编号
type: number; // 内容类型
content: string; // 消息内容JSON 格式)
status: number; // 消息状态(正常 / 已撤回)
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 // 客户端消息编号
receiverId: number // 接收人编号
type: number // 内容类型
content: string // 消息内容JSON 格式)
receipt?: boolean // 是否需要回执;不传后端默认 true普通私聊用户消息
/** 发送私聊消息 */
export function sendPrivateMessage(data: ImPrivateMessageApi.PrivateMessageSendReqVO) {
return requestClient.post<ImPrivateMessageApi.PrivateMessageRespVO>(
'/im/message/private/send',
data,
);
}
// 私聊历史消息列表 Request VO
export interface ImPrivateMessageListReqVO {
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 = (
/** 拉取私聊消息(增量) */
export function pullPrivateMessages(
params: { minId: number | string; size: number },
signal?: AbortSignal
) => {
return requestClient.get<ImPrivateMessageRespVO[]>('/im/message/private/pull', { params, signal })
signal?: AbortSignal,
) {
return requestClient.get<ImPrivateMessageApi.PrivateMessageRespVO[]>(
'/im/message/private/pull',
{ params, signal },
);
}
// 查询私聊历史消息
export const getPrivateMessageList = (params: ImPrivateMessageListReqVO) => {
return requestClient.get<ImPrivateMessageRespVO[]>('/im/message/private/list', { params })
/** 查询私聊历史消息 */
export function getPrivateMessageList(params: ImPrivateMessageApi.PrivateMessageListReqVO) {
return requestClient.get<ImPrivateMessageApi.PrivateMessageRespVO[]>(
'/im/message/private/list',
{ params },
);
}
// 标记私聊消息已读
export const readPrivateMessages = (receiverId: number | string, messageId: number | string) => {
return requestClient.put<boolean>('/im/message/private/read', undefined, { params: { receiverId, messageId } })
/** 标记私聊消息已读 */
export function readPrivateMessages(
receiverId: number | string,
messageId: number | string,
) {
return requestClient.put<boolean>('/im/message/private/read', undefined, {
params: { receiverId, messageId },
});
}
// 查询对方已读到我发的最大消息 id多端 / 离线后用于补齐已读状态)
export const getPrivateMaxReadMessageId = (peerId: number | string, signal?: AbortSignal) => {
return requestClient.get<null | number>('/im/message/private/max-read-message-id', { params: { peerId }, signal })
/** 查询对方已读到我发的最大消息 id多端 / 离线后用于补齐已读状态) */
export function getPrivateMaxReadMessageId(
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) => {
return requestClient.delete<ImPrivateMessageRespVO>('/im/message/private/recall', { params: { id } })
/** 撤回私聊消息 */
export function recallPrivateMessage(id: number | string) {
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 interface ImRtcCallCreateReqVO {
conversationType: number
mediaType: number
groupId?: number
inviteeIds: number[] // 被邀请的用户编号集合;私聊必传 1 个对端,群聊必传至少 1 人
export namespace ImRtcApi {
/** 创建新通话请求 VO */
export interface RtcCallCreateReqVO {
conversationType: number;
mediaType: number;
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 {
room: string
inviteeIds: number[]
/** 创建新通话;私聊或群聊根据 conversationType 区分 */
export function createCall(data: ImRtcApi.RtcCallCreateReqVO) {
return requestClient.post<ImRtcApi.RtcCallRespVO>('/im/rtc/create', data);
}
// 通话会话 VOcreate / join / accept / refreshToken 共用
export interface ImRtcCallRespVO {
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[]
/** 通话中追加邀请;仅群通话可用 */
export function inviteCall(data: ImRtcApi.RtcCallInviteReqVO) {
return requestClient.post<boolean>('/im/rtc/invite', data);
}
// 群活跃通话查询响应;不含 token
export interface ImRtcGroupCallRespVO {
room: string
groupId: number
mediaType: number
inviterId: number
joinedUserIds?: number[]
inviteeIds?: number[]
/** 加入已有群通话;用于胶囊条「加入」按钮 */
export function joinCall(room: string) {
return requestClient.post<ImRtcApi.RtcCallRespVO>('/im/rtc/join', undefined, {
params: { room },
});
}
// 创建新通话;私聊或群聊根据 conversationType 区分
export const createCall = (data: ImRtcCallCreateReqVO) => {
return requestClient.post<ImRtcCallRespVO>('/im/rtc/create', data)
/** 接听通话 */
export function acceptCall(room: string) {
return requestClient.post<ImRtcApi.RtcCallRespVO>('/im/rtc/accept', undefined, {
params: { room },
});
}
// 通话中追加邀请;仅群通话可用
export const inviteCall = (data: ImRtcCallInviteReqVO) => {
return requestClient.post<boolean>('/im/rtc/invite', data)
/** 拒绝通话 */
export function rejectCall(room: string) {
return requestClient.post<boolean>('/im/rtc/reject', undefined, {
params: { room },
});
}
// 加入已有群通话;用于胶囊条「加入」按钮
export const joinCall = (room: string) => {
return requestClient.post<ImRtcCallRespVO>('/im/rtc/join', undefined, { params: { room } })
/** 取消邀请;主叫接通前调用 */
export function cancelCall(room: string) {
return requestClient.post<boolean>('/im/rtc/cancel', undefined, {
params: { room },
});
}
// 接听通话
export const acceptCall = (room: string) => {
return requestClient.post<ImRtcCallRespVO>('/im/rtc/accept', undefined, { params: { room } })
/** 离开通话;接通后调用 */
export function leaveCall(room: string) {
return requestClient.post<boolean>('/im/rtc/leave', undefined, {
params: { room },
});
}
// 拒绝通话
export const rejectCall = (room: string) => {
return requestClient.post<boolean>('/im/rtc/reject', undefined, { params: { room } })
/** 振铃超时检查RUNNING 端 timer 兜底,触发后端立即扫描该 room 的超时 INVITING接口静默 */
export function noAnswerCallCheck(room: string) {
return requestClient.post<boolean>(
'/im/rtc/no-answer-call-check',
undefined,
{ params: { room } },
);
}
// 取消邀请;主叫接通前调用
export const cancelCall = (room: string) => {
return requestClient.post<boolean>('/im/rtc/cancel', undefined, { params: { room } })
}
// 离开通话;接通后调用
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 } })
/** 查询当前进行中的通话;目前仅群聊场景(胶囊条),返回 null 表示无活跃通话 */
export function getActiveCall(groupId: number) {
return requestClient.get<ImRtcApi.RtcGroupCallRespVO | null>(
'/im/rtc/get-active-call',
{ params: { groupId } },
);
}

View File

@ -9,16 +9,23 @@ export namespace SystemUserApi {
username: string;
nickname: string;
deptId: number;
deptName?: string;
postIds: string[];
email: string;
mobile: string;
sex: number;
avatar: string;
loginIp: string;
loginDate?: Date;
status: number;
remark: string;
createTime?: Date;
}
/** 用户精简信息 */
export interface UserSimple extends User {
id: number;
}
}
/** 查询用户管理列表 */
@ -86,3 +93,17 @@ export function updateUserStatus(id: number, status: number) {
export function getSimpleUserList() {
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 uiStore = useImUiStore()
const totalUnread = computed(() => conversationStore.getTotalUnreadCount) // Tab
const unhandledRequestCount = computed(() => friendStore.getUnhandledRequestCount) // Tab =
/** 消息 Tab 的红点:所有非免打扰会话的未读总和 */
const totalUnread = computed(() => conversationStore.getTotalUnreadCount)
/** 通讯录 Tab 的红点:未处理好友申请数(接收方=我) */
const unhandledRequestCount = computed(() => friendStore.getUnhandledRequestCount)
const tabs = [
{ name: 'ImHomeConversation', icon: 'ep:chat-round' },
{ name: 'ImHomeContact', icon: 'mingcute:contacts-line' }
] // 两个主 Tab用路由 name 而非 path避免前缀 / 嵌套调整后失效
/** 当前路由是否命中 Tab直接比对 route.name */
// Tab route.name
const isActive = (name: string) => route.name === name
/** 切换 Tab当前已选中时消息 Tab 触发"滚动到下一个未读"(对齐微信 PC其它 Tab 无动作 */
// Tab Tab "" PC Tab
const goTab = (name: string) => {
if (route.name === name) {
if (name === 'ImHomeConversation') {
@ -43,7 +45,7 @@ const goTab = (name: string) => {
router.push({ name })
}
/** 跳转个人中心(路由 name=Profile */
// name=Profile
const goProfile = () => router.push({ name: 'Profile' })
</script>

View File

@ -1,14 +1,15 @@
<script lang="ts" setup>
import type { SystemUserApi } from '#/api/system/user'
import { computed, ref } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons'
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 { useMessage } from '#/views/im/utils/message-feedback'
import { getSimpleUserListByNickname, type UserVO } from '#/views/im/utils/system-user'
import { ImFriendAddSource } from '../../../utils/constants'
import { getGenderColor, getGenderIcon } from '../../../utils/user'
@ -17,18 +18,14 @@ import UserAvatar from '../user/UserAvatar.vue'
defineOptions({ name: 'ImFriendAddDialog' })
// TODO @AI
const visible = ref(false)
/** 预填目标用户:非 null 时跳过搜索步骤,直接进入申请表单(群成员加好友 / 名片加好友等场景) */
const presetUser = ref<null | UserVO>(null)
/** 添加来源;参见 ImFriendAddSourceEnum默认 SEARCH */
const addSource = ref<number>(ImFriendAddSource.SEARCH)
/** 来源附带信息addSource=ImFriendAddSource.GROUP 时传群名,话术拼为「我是 XX 群的 YY」 */
const addSourceExtra = ref<string>('')
const visible = ref(false) //
const presetUser = ref<null | SystemUserApi.UserSimple>(null) // 预填目标用户:非 null 时跳过搜索步骤,直接进入申请表单(群成员加好友 / 名片加好友等场景
const addSource = ref<number>(ImFriendAddSource.SEARCH) // ImFriendAddSourceEnum SEARCH
const addSourceExtra = ref<string>('') // addSource=ImFriendAddSource.GROUP XX YY
defineExpose({
/** 打开加好友弹窗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
addSource.value = opts?.addSource ?? ImFriendAddSource.SEARCH
addSourceExtra.value = opts?.addSourceExtra ?? ''
@ -39,7 +36,6 @@ defineExpose({
const friendStore = useFriendStore()
const userStore = useUserStore()
const message = useMessage()
/** 当前登录用户编号;用 computed 包一层,切账号后随 wsCache 重取,避免顶层求值在 keep-alive 实例里持有旧 id */
const currentUserId = computed(() => getCurrentUserId())
@ -48,20 +44,15 @@ const currentUserId = computed(() => getCurrentUserId())
const visibleUsers = computed(() =>
users.value.filter((user) => user.id !== currentUserId.value)
)
const keyword = ref('')
const users = ref<UserVO[]>([])
const searched = ref(false)
const loading = ref(false)
/** 当前步骤search=搜索列表apply=申请表单 */
const step = ref<'apply' | 'search'>('search')
/** 申请目标用户 */
const targetUser = ref<null | UserVO>(null)
/** 申请理由(默认填「我是 ${当前昵称}」,对齐微信交互) */
const applyContent = ref('')
/** 对接收方的备注(仅自己可见) */
const displayName = ref('')
const submitting = ref(false)
const keyword = ref('') //
const users = ref<SystemUserApi.UserSimple[]>([]) //
const searched = ref(false) //
const loading = ref(false) //
const step = ref<'apply' | 'search'>('search') // search=apply=
const targetUser = ref<null | SystemUserApi.UserSimple>(null) //
const applyContent = ref('') // ${}
const displayName = ref('') //
const submitting = ref(false) //
/** 弹窗标题随步骤切换 */
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
const myNickname = userStore.userInfo?.nickname || ''
applyContent.value = myNickname ? `我是${myNickname}` : ''
@ -184,57 +175,57 @@ async function handleSubmitApply() {
<Spin :spinning="loading" wrapper-class-name="w-full">
<div class="h-[400px] mt-2.5">
<div
v-if="visibleUsers.length === 0"
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)"
<div
v-if="visibleUsers.length === 0"
class="py-10 text-13px text-center text-[var(--ant-color-text-disabled)]"
>
添加
</Button>
<Button v-else size="small" disabled>已添加</Button>
</div>
{{ 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)"
>
添加
</Button>
<Button v-else size="small" disabled>已添加</Button>
</div>
</div>
</Spin>
</template>
@ -272,7 +263,7 @@ async function handleSubmitApply() {
:maxlength="255"
show-count
placeholder="请填写申请理由"
/>
/>
<div class="text-13px text-[var(--ant-color-text-secondary)] mt-3 mb-1.5">备注</div>
<Input

View File

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

View File

@ -27,11 +27,13 @@ const props = withDefaults(
url?: string // URL
}>(),
{
size: 42,
radius: '15%',
clickable: false,
name: '',
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 { Button, Modal } from 'ant-design-vue'
import { Button, message, Modal } from 'ant-design-vue'
import { createGroup } from '#/api/im/group'
import { useMessage } from '#/views/im/utils/message-feedback'
import { buildDefaultGroupName } from '../../../utils/group'
import { useFriendStore } from '../../store/friendStore'
@ -20,7 +19,6 @@ const emit = defineEmits<{
created: [groupId: number]
}>()
const message = useMessage()
const friendStore = useFriendStore()
const groupStore = useGroupStore()

View File

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

View File

@ -5,13 +5,12 @@ import { computed, ref } from 'vue'
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 { getCurrentUserId } from '#/views/im/utils/auth'
import { GROUP_MAX_MEMBER } from '#/views/im/utils/config'
import { ImGroupMemberRole } from '#/views/im/utils/constants'
import { useMessage } from '#/views/im/utils/message-feedback'
import { useFriendStore } from '../../store/friendStore'
import { useGroupStore } from '../../store/groupStore'
@ -24,7 +23,6 @@ const emit = defineEmits<{
reload: [friendIds: number[]]
}>()
const message = useMessage()
const friendStore = useFriendStore()
const groupStore = useGroupStore()

View File

@ -3,10 +3,9 @@ import type { GroupMemberLite } from './GroupMember.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 { useMessage } from '#/views/im/utils/message-feedback'
import GroupMemberPickerPanel from '../picker/GroupMemberPickerPanel.vue'
@ -17,8 +16,6 @@ const emit = defineEmits<{
reload: []
}>()
const message = useMessage()
const visible = ref(false)
const submitting = ref(false)
const groupId = ref(0)

View File

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

View File

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

View File

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

View File

@ -5,9 +5,7 @@ import { computed, ref } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons'
import { Input } from 'ant-design-vue'
import { useMessage } from '#/views/im/utils/message-feedback'
import { Input, message } from 'ant-design-vue'
import { ImConversationType } from '../../../utils/constants'
import { filterConversationsByKeyword, getConversationKey } from '../../../utils/conversation'
@ -40,17 +38,14 @@ const props = withDefaults(
)
const emit = defineEmits<{
'create-chat': []
createChat: []
/** 用户在「最近转发」段进入移除模式后点 ×;业务壳收到后调 conversationStore.removeRecentForwardConversationKey 落盘 */
'remove-recent': [key: string]
removeRecent: [key: string]
'update:selectedKeys': [value: string[]]
}>()
const message = useMessage()
const keyword = ref('')
/** 「最近转发」段是否处于移除模式true 时头像右上角变 × 不再切勾选 */
const recentRemoveMode = ref(false)
const recentRemoveMode = ref(false) // true ×
/** 全量会话的 key→Conversation 映射,已选 / 最近转发反查共用,避免每次 O(N) 扫 */
const byKey = computed(() => {
@ -200,7 +195,7 @@ function handleToggle(conversation: Conversation) {
<span
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)]"
@click.stop="emit('remove-recent', getConversationKey(conversation))"
@click.stop="emit('removeRecent', getConversationKey(conversation))"
>
<Icon icon="ant-design:close-outlined" :size="10" />
</span>
@ -235,7 +230,7 @@ function handleToggle(conversation: Conversation) {
<div
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)]"
@click="emit('create-chat')"
@click="emit('createChat')"
>
<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)]"

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import type { CallParticipantVM } from './RtcCallParticipantTile.vue'
import { computed, ref, watch } from 'vue'
import { useIntervalFn } from '@vueuse/core'
import { message } from 'ant-design-vue'
import { Track } from 'livekit-client'
import {
@ -21,7 +22,6 @@ import {
ImRtcCallMediaType,
ImRtcCallStage
} from '#/views/im/utils/constants'
import { useMessage } from '#/views/im/utils/message-feedback'
import { getSenderAvatar, getSenderDisplayName } from '#/views/im/utils/user'
import { useLiveKitRoom } from '../../composables/useLiveKitRoom'
@ -34,7 +34,6 @@ import RtcCallRunning from './RtcCallRunning.vue'
defineOptions({ name: 'ImRtcCallContainer' })
const rtcStore = useRtcStore()
const message = useMessage()
const lk = useLiveKitRoom()
const memberPickerRef = ref<InstanceType<typeof RtcCallMemberPickerDialog>>()
@ -123,16 +122,13 @@ const participants = computed<CallParticipantVM[]>(() => {
const conversationType = call.conversationType
const targetId = call.groupId ?? 0
const myId = getCurrentUserId()
const result: CallParticipantVM[] = []
//
result.push({
const result: CallParticipantVM[] = [{
userId: myId,
nickname: getSenderDisplayName(myId, conversationType, targetId),
avatar: getSenderAvatar(myId, conversationType, targetId) || undefined,
isLocal: true,
videoStream: localStream.value
})
}]
// Camera
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)
/** 群通话成员;缓存为空时用 INVITE 载荷里的主叫兜底,避免空白 */
// INVITE
const callMembers = useGroupCallMembers(
computed(() => (props.isGroup ? props.payload?.groupId : undefined)),
computed(() => props.payload?.inviterUserId)

View File

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

View File

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

View File

@ -5,11 +5,10 @@ import { DICT_TYPE } from '@vben/constants'
import { getDictLabel } from '@vben/hooks'
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 { getCurrentUserId } from '#/views/im/utils/auth'
import { useMessage } from '#/views/im/utils/message-feedback'
import { useGroupCallMembers } from '../../composables/useGroupCallMembers'
import { useRtcStore } from '../../store/rtcStore'
@ -22,7 +21,6 @@ const props = defineProps<{
}>()
const rtcStore = useRtcStore()
const message = useMessage()
const popoverVisible = ref(false)

View File

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

View File

@ -1,21 +1,20 @@
<script lang="ts" setup>
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 { getDictLabel } from '@vben/hooks'
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 UserVO } from '#/views/im/utils/system-user'
import { getSimpleUser, type SystemUserApi } from '#/api/system/user'
import { formatDate } from '#/views/im/utils/time'
import { ImFriendAddSource } from '../../../utils/constants'
import { toUserCardTarget } from '../../../utils/message'
import { confirmDeleteFriend } from '../../../utils/message-feedback'
import { getGenderColor, getGenderIcon } from '../../../utils/user'
import { useFriendStore } from '../../store/friendStore'
import FriendAddDialog from '../friend/FriendAddDialog.vue'
@ -39,9 +38,11 @@ const props = withDefaults(
user: null | User
}>(),
{
relation: 'readonly',
addSource: ImFriendAddSource.SEARCH,
addSourceExtra: '',
displayName: '',
previewZIndex: 2000,
addSource: ImFriendAddSource.SEARCH
relation: 'readonly'
}
)
@ -63,11 +64,9 @@ const emit = defineEmits<{
*/
export type UserInfoRelation = 'friend' | 'readonly' | 'self' | 'stranger'
const message = useMessage()
const friendStore = useFriendStore()
/** 起手 user + getSimpleUser 合并后的完整对象(性别 / 部门补齐用) */
const full = ref<null | User>(props.user)
const full = ref<null | User>(props.user) // 起手 user + getSimpleUser 合并后的完整对象(性别 / 部门补齐用
/** 主标题:备注优先(好友场景),其次原昵称 */
const headerName = computed(() => props.displayName || full.value?.nickname || '')
@ -85,8 +84,7 @@ const friendInfo = computed(() =>
/** 是否已拉黑:菜单项「加入黑名单 / 移出黑名单」按这个切换 */
const isBlocked = computed(() => !!friendInfo.value?.blocked)
/** 备注内联编辑editingRemark 控制输入态user 切换时由下面的 watch 复位避免脏态泄漏 */
const editingRemark = ref(false)
const editingRemark = ref(false) // editingRemark user watch
const remarkInput = ref('')
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>>()
const friendAddDialogRef = ref<InstanceType<typeof FriendAddDialog>>() // refhandleAddFriend open({ presetUser, addSource, addSourceExtra })
/** 推荐名片弹窗 refhandleRecommend 调用 open({ target }) 打开 */
const recommendDialogRef = ref<InstanceType<typeof RecommendCardDialog>>()
const recommendDialogRef = ref<InstanceType<typeof RecommendCardDialog>>() // refhandleRecommend open({ target })
/** 把他推荐给朋友:弹 RecommendCardDialog 选目标会话 */
function handleRecommend() {
if (!props.user?.id) {
@ -191,14 +187,14 @@ function handleAddFriend() {
if (!props.user?.id) {
return
}
const presetUser: UserVO = {
const presetUser: SystemUserApi.UserSimple = {
id: props.user.id,
nickname: props.user.nickname,
avatar: props.user.avatar,
sex: props.user.sex,
deptId: props.user.deptId,
deptName: props.user.deptName
} as UserVO
} as SystemUserApi.UserSimple
friendAddDialogRef.value?.open({
presetUser,
addSource: props.addSource,
@ -213,7 +209,7 @@ async function handleBlock() {
}
const target = props.user
try {
await message.confirm(`确定将「${target.nickname || ''}」加入黑名单吗?`, '加入黑名单')
await confirm(`确定将「${target.nickname || ''}」加入黑名单吗?`, '加入黑名单')
} catch {
return
}
@ -238,7 +234,25 @@ async function handleDeleteFriend() {
const target = props.user
const clearConversation = ref(true)
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 {
return
}

View File

@ -2,10 +2,12 @@ import type { Conversation, Message } from '../types'
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 { useMessage } from '#/views/im/utils/message-feedback'
import { updateFile } from '#/views/im/utils/upload'
import { isOpenableUrl } from '#/views/im/utils/url'
import {
MESSAGE_FILE_MAX_MB,
@ -123,7 +125,7 @@ export interface UploadAndSendMediaOptions {
*
* useMessageSender.sendRaw ack
* 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
* 4. sendRaw(existingClientMessageId)
*
@ -168,7 +170,6 @@ export const useMediaUploader = () => {
const conversationStore = useConversationStore()
const messageStore = useMessageStore()
const muteOverlay = useMuteOverlay()
const message = useMessage()
const { sendRaw } = useMessageSender()
/**
@ -361,13 +362,10 @@ export const useMediaUploader = () => {
// 2. 上传:进度回调 patch uploadProgress失败保留 _localFile 供重试
let url: string | undefined
try {
const form = new FormData()
form.append('file', opts.file)
const res = (await updateFile(
form,
url = await uploadFile(
{ file: opts.file },
createUploadProgressHandler(conversation, clientMessageId)
)) as { data?: string }
url = res?.data
)
} catch (error) {
console.error(`[IM] ${handler.kind}上传失败`, error)
}

View File

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

View File

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

View File

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

View File

@ -4,11 +4,10 @@ import type { FriendLite, FriendRequest, Group, GroupLite, User } from '../../ty
import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { confirm } from '@vben/common-ui'
import { IconifyIcon as Icon } from '@vben/icons'
import { Input } from 'ant-design-vue'
import { useMessage } from '#/views/im/utils/message-feedback'
import { Input, message } from 'ant-design-vue'
import { ImConversationType } from '../../../utils/constants'
import { StorageKeys } from '../../../utils/db'
@ -30,7 +29,6 @@ const router = useRouter()
const conversationStore = useConversationStore()
const friendStore = useFriendStore()
const groupStore = useGroupStore()
const message = useMessage()
/** 用 type 判别选中是好友 / 群聊 / 好友申请 */
type Selection =
@ -204,7 +202,7 @@ function handleChatGroup(group: GroupLite) {
/** 删除好友:二次确认 → store 落库 → 清空当前选中 */
async function handleDeleteFriend(friend: FriendLite) {
try {
await message.confirm(`确定删除好友「${friend.nickname}」吗?`, '删除联系人')
await confirm(`确定删除好友「${friend.nickname}」吗?`, '删除联系人')
// friendStore.deleteFriend
await friendStore.deleteFriend(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 { confirm } from '@vben/common-ui'
import { CommonStatusEnum } from '@vben/constants'
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 {
dissolveGroup,
@ -18,7 +19,6 @@ import { quitGroup, updateGroupMember } from '#/api/im/group/member'
import { getCurrentUserId } from '#/views/im/utils/auth'
import { ImConversationType, ImGroupMemberRole } from '#/views/im/utils/constants'
import { toGroupCardTarget } from '#/views/im/utils/message'
import { useMessage } from '#/views/im/utils/message-feedback'
import { isGroupQuit } from '#/views/im/utils/user'
import GroupAdminSetDialog from '../../../../components/group/GroupAdminSetDialog.vue'
@ -59,7 +59,6 @@ const emit = defineEmits<{
const MEMBER_PREVIEW_COUNT = 14
const conversationStore = useConversationStore()
const groupStore = useGroupStore()
const message = useMessage()
const visible = computed({
get: () => props.modelValue,
@ -71,8 +70,7 @@ const visible = computed({
const searchText = ref('')
const showAllMembers = ref(false)
/** 邀请好友入群弹窗 refhandleOpenInvite 调 open({ groupId }) 打开 */
const inviteDialogRef = ref<InstanceType<typeof GroupMemberAddDialog>>()
const inviteDialogRef = ref<InstanceType<typeof GroupMemberAddDialog>>() // refhandleOpenInvite open({ groupId })
/** 打开邀请好友入群弹窗 */
function handleOpenInvite() {
if (!props.group?.id) {
@ -99,7 +97,7 @@ const isOwnerOrAdmin = computed(
(myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN)
)
// 退 + userId
/** 排除已退群成员 + 关键字过滤;按角色排序:群主→管理员→普通成员(同角色按 userId 稳定) */
const visibleMembers = computed(() => {
return props.members
.filter(
@ -114,7 +112,7 @@ const visibleMembers = computed(() => {
})
})
// / N
/** 折叠规则:搜索 / 已展开 时不折叠,其余只取前 N 个 */
const moreMembersHidden = computed(
() =>
!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>>()
const requestListDialogRef = ref<InstanceType<typeof GroupRequestListDialog>>() // refhandleOpenRequestList open({ groupId })
/** 打开当前群的进群申请列表 */
function handleOpenRequestList() {
@ -297,8 +294,7 @@ function handleOpenRequestList() {
// ==================== ====================
/** 分享群名片弹窗 refhandleShareGroupCard 调用 open({ target }) 打开 */
const recommendCardDialogRef = ref<InstanceType<typeof RecommendCardDialog>>()
const recommendCardDialogRef = ref<InstanceType<typeof RecommendCardDialog>>() // refhandleShareGroupCard open({ target })
/** 分享群名片:把当前群作为名片消息推荐给其他会话 */
function handleShareGroupCard() {
@ -316,9 +312,9 @@ async function handleQuit() {
if (!props.group) {
return
}
// message.confirm reject return
//
try {
await message.confirm('退出群聊后将不再接收群里的消息,确认退出吗?', '确认退出')
await confirm('退出群聊后将不再接收群里的消息,确认退出吗?', '确认退出')
} catch {
return
}
@ -340,7 +336,7 @@ async function handleDissolve() {
return
}
try {
await message.confirm('解散后所有成员将被移出,且无法恢复,确认解散吗?', '确认解散')
await confirm('解散后所有成员将被移出,且无法恢复,确认解散吗?', '确认解散')
} catch {
return
}
@ -355,12 +351,9 @@ async function handleDissolve() {
// ==================== ====================
// / + +
/** 移除群成员弹窗 ref */
const removeDialogRef = ref<InstanceType<typeof GroupMemberRemoveDialog>>()
/** 设置群管理员弹窗 ref */
const adminSetDialogRef = ref<InstanceType<typeof GroupAdminSetDialog>>()
/** 转让群主弹窗 ref */
const ownerTransferDialogRef = ref<InstanceType<typeof GroupOwnerTransferDialog>>()
const removeDialogRef = ref<InstanceType<typeof GroupMemberRemoveDialog>>() // ref
const adminSetDialogRef = ref<InstanceType<typeof GroupAdminSetDialog>>() // ref
const ownerTransferDialogRef = ref<InstanceType<typeof GroupOwnerTransferDialog>>() // ref
// ---------- ----------
@ -434,7 +427,7 @@ function handleOpenTransferOwner() {
>
<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)]">
<Input v-model:value="searchText" placeholder="搜索群成员" allow-clear>
@ -772,7 +765,7 @@ function handleOpenTransferOwner() {
<!-- ==================== 底部退出 / 解散群聊历史退群群隐藏已退群无需再退 ==================== -->
<div
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
@ -820,6 +813,10 @@ function handleOpenTransferOwner() {
background-color: var(--ant-color-primary-bg);
}
.im-conversation-group-side__modal {
--im-conversation-side-bg: #f5f7fa;
}
/* :deep 穿透 Icon 内部 svg el-icon 全局 color 在暗色模式下被主题盖过,锁 fill 到当前色 */
.im-conversation-group-side__icon-tile :deep(svg) {
fill: currentColor !important;
@ -827,14 +824,17 @@ function handleOpenTransferOwner() {
/* 相邻信息行加分隔线; 相邻兄弟选择器无法用工具类表达 */
.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>
<!-- el-drawer append-to-body 后被传送出当前 scoped 边界scoped CSS data-v 不会落到 body
这里靠 modal-class .el-overlay .el-drawer__body 的祖先写一段全局规则压掉默认 padding -->
<!-- AntD Drawer 被传送出当前 scoped 边界这里靠 root-class-name 压掉默认 padding -->
<style>
.im-conversation-group-side__modal .el-drawer__body {
.im-conversation-group-side__modal .ant-drawer-body {
padding: 0;
}
</style>

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ const props = withDefaults(
canAtAll?: boolean // 当前用户是否能 @ 全员(群主 / 管理员父组件按角色算好传入
members: GroupMemberLite[] //
// x + top / bottom bottom picker 沿 @
position: { bottom?: number; top?: number; x: number; }
position?: { bottom?: number; top?: number; x: number; }
searchText?: string // @
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 { 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 {
ensureMediaSizeWithinLimit,
useMediaUploader
@ -28,9 +30,6 @@ import {
serializeMessage,
withQuotePayload
} 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 ReplyPreview from '../message/ReplyPreview.vue'
@ -43,7 +42,6 @@ defineOptions({ name: 'ImMessageInput' })
const conversationStore = useConversationStore()
const groupStore = useGroupStore()
const friendStore = useFriendStore()
const message = useMessage()
const { send, sendRaw } = useMessageSender()
const {
uploadAndSendMedia,
@ -518,8 +516,7 @@ const mentionPosition = ref<{ bottom?: number; top?: number; x: number; }>({ x:
/** MentionPicker w-50 沿
* 高度不再用常量算位置bottom 锚定后 picker 内容多寡都不影响下沿位置自然贴 @ */
const MENTION_WIDTH = 200
/** 上方剩余空间至少这么多才放上方,否则翻到下方(避免 picker 被视口顶 / 顶部 chat header 切掉) */
const MENTION_MIN_FIT_ABOVE = 120
const MENTION_MIN_FIT_ABOVE = 120 // 上方剩余空间至少这么多才放上方,否则翻到下方(避免 picker 被视口顶 / 顶部 chat header 切掉
/** 当前 @ 关键词在 editor 里的范围onMentionSelect 用它定位删除 + 插入 token */
let mentionRange: null | Range = null
@ -913,17 +910,15 @@ async function uploadAndSendVideo(file: File) {
// 2. probe probe cover
// 2.1 async IIFE await lint floating promise
// url=undefined step 3 url
const videoForm = new FormData()
videoForm.append('file', file)
const videoUploadPromise: Promise<{ data?: string }> = (async () => {
const videoUploadPromise: Promise<string | undefined> = (async () => {
try {
return (await updateFile(
videoForm,
return await uploadFile(
{ file },
createUploadProgressHandler(conversation, clientMessageId)
)) as { data?: string }
)
} catch (error) {
console.warn('[IM] 视频本体上传失败', error)
return { data: undefined }
return undefined
}
})()
// 2.2 probe + blob probe
@ -937,12 +932,9 @@ async function uploadAndSendVideo(file: File) {
return { probe, coverUrl: undefined as string | undefined }
}
try {
const coverForm = new FormData()
coverForm.append(
'file',
new File([probe.cover], `cover-${Date.now()}.jpg`, { type: 'image/jpeg' })
)
const coverUrl = ((await updateFile(coverForm)) as { data?: string })?.data || undefined
const coverUrl = await uploadFile({
file: new File([probe.cover], `cover-${Date.now()}.jpg`, { type: 'image/jpeg' })
})
return { probe, coverUrl }
} catch (error) {
console.warn('[IM] 视频封面上传失败', error)
@ -957,7 +949,7 @@ async function uploadAndSendVideo(file: File) {
coverUploadPromise
])
// 3.2 url FAILED / _localFile
const url = videoRes?.data
const url = videoRes
if (!url) {
markMediaFailed(conversation.type, conversation.targetId, clientMessageId)
return
@ -1017,11 +1009,11 @@ async function onVideoPicked(e: Event) {
<!-- 禁言 / 封禁覆盖层优先级 封禁 > 全群禁言 > 成员禁言 -->
<div
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="
muteOverlay.icon === 'ant-design:stop-outlined'
? 'text-[var(--ant-color-error-active)] bg-[var(--ant-color-error-bg)] border-[var(--ant-color-error-hover)]'
: 'text-[var(--ant-color-warning-active)] bg-[var(--ant-color-warning-bg)] border-[var(--ant-color-warning-hover)]'
? 'message-input__mute-overlay--error'
: 'message-input__mute-overlay--warning'
"
>
<Icon :icon="muteOverlay.icon" :size="18" />
@ -1182,6 +1174,30 @@ async function onVideoPicked(e: Event) {
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 维护 */
.message-input__editor[data-empty]::before {
content: attr(data-placeholder);

View File

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

View File

@ -9,9 +9,8 @@ import {
watch
} 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'
defineOptions({ name: 'ImVoiceRecorder' })
@ -31,8 +30,6 @@ const emit = defineEmits<{
'update:modelValue': [value: boolean]
}>()
const message = useMessage()
const VOICE_MIME_TYPE_OPTIONS = [
{ extension: 'webm', mimeType: 'audio/webm;codecs=opus' },
{ 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 { Button } from 'ant-design-vue'
import { Button, message } from 'ant-design-vue'
import { unpinGroupMessage as apiUnpinGroupMessage } from '#/api/im/group'
import { getCurrentUserId } from '#/views/im/utils/auth'
import { ImConversationType, ImGroupMemberRole } from '#/views/im/utils/constants'
import { resolveConversationLastContent } from '#/views/im/utils/conversation'
import { useMessage } from '#/views/im/utils/message-feedback'
import { getSenderDisplayName, isGroupQuit } from '#/views/im/utils/user'
import { useGroupStore } from '../../../../store/groupStore'
@ -29,7 +28,6 @@ const emit = defineEmits<{
}>()
const groupStore = useGroupStore()
const message = useMessage()
/** 当前群(含 pinnedMessages */
const group = computed(() => groupStore.getGroup(props.groupId))

View File

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

View File

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

View File

@ -2,7 +2,7 @@
import { computed, onBeforeUnmount } from 'vue'
import { IconifyIcon as Icon } from '@vben/icons'
import { formatFileSize } from '@vben/utils'
import { formatFileSize, openSafeUrl } from '@vben/utils'
import { Image } from 'ant-design-vue'
@ -26,7 +26,6 @@ import {
type VideoMessage
} from '#/views/im/utils/message'
import { formatSeconds } from '#/views/im/utils/time'
import { openSafeUrl } from '#/views/im/utils/url'
import MaterialBubble from './MaterialBubble.vue'
import TipSegments from './TipSegments.vue'
@ -48,9 +47,9 @@ const props = defineProps<{
const emit = defineEmits<{
/** 名片点击:调用方决定弹卡片 / 跳群等行为 */
'click-card': [card: CardMessage, e: MouseEvent]
clickCard: [card: CardMessage, e: MouseEvent]
/** 合并消息气泡点击:调用方决定开 dialog 或栈内 push */
'open-merge': [content: string]
openMerge: [content: string]
}>()
/** 各 type 判定 */
@ -117,8 +116,7 @@ const mergePreviewLines = computed(() => {
.map((item) => `${item.senderNickname}${summarizeMessageContent(item)}`)
})
/** 表情 payload非法宽高派生成 undefined让 <img> 走 CSS max-w / max-h 兜底 */
const FACE_DIMENSION_MAX = 2048
const FACE_DIMENSION_MAX = 2048 // 表情 payload非法宽高派生成 undefined让 <img> 走 CSS max-w / max-h 兜底
const facePayload = computed(() => {
if (!isFace.value) {
return null
@ -143,6 +141,7 @@ function bubbleClass(variant: 'file' | 'text' | 'voice'): string[] {
case 'file': {
return [
side,
'message-bubble--file',
isSelf
? 'bg-[#95ec69] border-[var(--ant-color-border-secondary)]'
: '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': {
return [
side,
'message-bubble--text',
isSelf
? 'text-black bg-[#95ec69]'
: 'text-[var(--ant-color-text)] bg-[var(--ant-color-fill-secondary)]'
: 'text-[var(--ant-color-text)]'
]
}
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)
}
/** 语音点击:托管给 useVoicePlayer 全局互斥播放,新点的语音会停掉旧的 */
const voicePlayer = useVoicePlayer()
const voicePlayer = useVoicePlayer() // useVoicePlayer
/**
* 实例级唯一播放 key每个 MessageBubble 实例独立一份
*
@ -308,14 +307,14 @@ onBeforeUnmount(() => {
v-else-if="isCard && cardPayload"
:card="cardPayload"
clickable
@click="(e: MouseEvent) => emit('click-card', cardPayload!, e)"
@click="(e: MouseEvent) => emit('clickCard', cardPayload!, e)"
/>
<!-- 合并转发气泡title + 摘要预览 + 底部聊天记录标签 -->
<div
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]"
@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">
{{ mergePayload.title }}
@ -367,10 +366,17 @@ onBeforeUnmount(() => {
height: 0;
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 {
left: -5px;
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 {
right: -5px;
@ -378,6 +384,10 @@ onBeforeUnmount(() => {
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 锁死 */
.message-bubble__voice-icon :deep(svg) {
fill: #606266 !important;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,8 +17,7 @@ defineOptions({ name: 'ImMessageMergeDetailDialog' })
const voicePlayer = useVoicePlayer()
const visible = ref(false)
/** 嵌套层级栈,存 parsed payload 避免切层重 parse */
const stack = ref<MergeMessage[]>([])
const stack = ref<MergeMessage[]>([]) // parsed payload parse
defineExpose({
/** 打开详情弹窗,传入顶层合并消息 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(
localStorage.getItem(StorageKeys.localStorage.conversationPinnedExpanded) === 'true'
)
@ -105,13 +104,11 @@ const showPinnedSection = computed(
// ==================== ====================
/** 添加朋友弹窗 ref右上角 +-下拉「添加朋友」入口调 open() 触发 */
const friendAddDialogRef = ref<InstanceType<typeof FriendAddDialog>>()
const friendAddDialogRef = ref<InstanceType<typeof FriendAddDialog>>() // ref +- open()
// ==================== ====================
/** 发起群聊弹窗 refhandleOpenCreateGroup 调 open() 打开 */
const createGroupDialogRef = ref<InstanceType<typeof GroupCreateDialog>>()
const createGroupDialogRef = ref<InstanceType<typeof GroupCreateDialog>>() // refhandleOpenCreateGroup open()
/** 打开发起群聊弹窗:无锁定项的全局入口 */
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 { getSimpleChannelList, type ImManagerChannelVO } from '#/api/im/manager/channel'
import { getSimpleChannelList } from '#/api/im/manager/channel'
import { ImConversationType } from '../../utils/constants'
import { getDb } from '../../utils/db'
@ -16,12 +16,12 @@ import { useConversationStore } from './conversationStore'
*/
export const useChannelStore = defineStore('imChannelStore', {
state: () => ({
channels: [] as ImManagerChannelVO[],
channels: [] as ImManagerChannelApi.Channel[],
loaded: false
}),
getters: {
getChannel(state): (id: number) => ImManagerChannelVO | undefined {
getChannel(state): (id: number) => ImManagerChannelApi.Channel | undefined {
return (id: number) => state.channels.find((c) => c.id === id)
}
},
@ -32,7 +32,7 @@ export const useChannelStore = defineStore('imChannelStore', {
/** 从 IndexedDB 恢复频道列表 */
async loadChannelList(): Promise<boolean> {
try {
const cached = await getDb().getAll<ChannelDO>('channels')
const cached = await getDb().getAll<ImManagerChannelApi.Channel>('channels')
if (!cached || cached.length === 0) {
return false
}

View File

@ -6,7 +6,7 @@ import type {
MessageDO
} from '../types'
import type { ImConversationReadRespVO } from '#/api/im/conversation/read'
import type { ImConversationReadApi } from '#/api/im/conversation/read'
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
}
@ -351,7 +351,7 @@ export const useConversationStore = defineStore('imConversationStore', {
/** 应用会话读位置 */
async applyConversationReadList(
records: ImConversationReadRespVO[],
records: ImConversationReadApi.ConversationReadRespVO[],
isActive?: () => boolean
): Promise<void> {
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 { acceptHMRUpdate, defineStore } from 'pinia'
import {
getFacePackList as apiGetFacePackList,
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'
import { getFacePackList as apiGetFacePackList } from '#/api/im/face/pack'
import { createFaceUserItem as apiCreateFaceUserItem, deleteFaceUserItem as apiDeleteFaceUserItem, getFaceUserItemList as apiGetFaceUserItemList } from '#/api/im/face/useritem'
/**
* IM store +
@ -24,9 +18,9 @@ import {
export const useFaceStore = defineStore('imFace', () => {
/** 系统表情包列表(含每个包的 items运营管理后台维护 */
const facePacks = ref<ImFacePackUserVO[]>([])
const facePacks = ref<ImFacePackApi.FacePackUser[]>([])
/** 个人表情包列表(用户长按「添加到表情」/ 上传产生) */
const faceUserItems = ref<ImFaceUserItemVO[]>([])
const faceUserItems = ref<ImFaceUserItemApi.FaceUserItem[]>([])
/** clear() 时递增;旧账号请求返回后不写入新账号内存 */
let storeEpoch = 0
@ -89,7 +83,7 @@ export const useFaceStore = defineStore('imFace', () => {
*
* 1. + 2.
*/
async function addFaceUserItem(reqVO: ImFaceUserItemSaveReqVO): Promise<boolean> {
async function addFaceUserItem(reqVO: ImFaceUserItemApi.FaceUserItemSaveReqVO): Promise<boolean> {
const requestEpoch = storeEpoch
const id = await apiCreateFaceUserItem(reqVO)
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 { acceptHMRUpdate, defineStore } from 'pinia'
import {
blockFriend as apiBlockFriend,
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 { 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'
import { agreeFriendRequest as apiAgreeFriendRequest, applyFriendRequest as apiApplyFriendRequest, getMyFriendRequest as apiGetMyFriendRequest, getMyFriendRequestList as apiGetMyFriendRequestList, pullMyFriendRequestList as apiPullMyFriendRequestList, refuseFriendRequest as apiRefuseFriendRequest } from '#/api/im/friend/request'
import { getCurrentUserId } from '#/views/im/utils/auth'
import { FRIEND_REQUEST_PAGE_SIZE } from '../../utils/config'
@ -155,8 +140,8 @@ export const useFriendStore = defineStore('imFriendStore', {
async loadFriendData(): Promise<boolean> {
try {
const [friends, friendRequests] = await Promise.all([
getDb().getAll<FriendDO>('friends'),
getDb().getAll<FriendRequestDO>('friendRequests')
getDb().getAll<Friend>('friends'),
getDb().getAll<FriendRequest>('friendRequests')
])
if (friends.length > 0) {
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)
},
@ -816,7 +801,7 @@ export const useFriendStore = defineStore('imFriendStore', {
}
})
function convertFriend(vo: ImFriendRespVO): Friend {
function convertFriend(vo: ImFriendApi.FriendRespVO): Friend {
return {
id: vo.id,
friendUserId: vo.friendUserId,
@ -835,7 +820,7 @@ function convertFriend(vo: ImFriendRespVO): Friend {
}
}
function convertFriendRequest(vo: ImFriendRequestRespVO): FriendRequest {
function convertFriendRequest(vo: ImFriendRequestApi.FriendRequestRespVO): FriendRequest {
return {
id: vo.id,
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 {
agreeGroupRequest as apiAgreeGroupRequest,
getMyGroupRequest as apiGetMyGroupRequest,
getUnhandledRequestList as apiGetUnhandledRequestList,
pullMyGroupRequestList as apiPullMyGroupRequestList,
refuseGroupRequest as apiRefuseGroupRequest,
type ImGroupRequestRespVO
} from '#/api/im/group/request'
import { agreeGroupRequest as apiAgreeGroupRequest, getMyGroupRequest as apiGetMyGroupRequest, getUnhandledRequestList as apiGetUnhandledRequestList, pullMyGroupRequestList as apiPullMyGroupRequestList, refuseGroupRequest as apiRefuseGroupRequest } from '#/api/im/group/request'
import { getCurrentUserId } from '#/views/im/utils/auth'
import { ImGroupRequestHandleResult } from '#/views/im/utils/constants'
@ -26,7 +19,7 @@ let pendingUnhandledFetch: null | PendingRequest = null
* IM Store
*
* unhandledList
* / Drawer count ImGroupRespVO pendingRequestCount
* / Drawer count ImGroupApi.GroupRespVO pendingRequestCount
*
*
* - IM fetchUnhandledGroupRequestList
@ -38,7 +31,7 @@ let pendingUnhandledFetch: null | PendingRequest = null
export const useGroupRequestStore = defineStore('imGroupRequestStore', {
state: () => ({
/** 我管理的所有群下未处理申请列表(按 id 倒序) */
unhandledList: [] as ImGroupRequestRespVO[],
unhandledList: [] as ImGroupRequestApi.GroupRequestRespVO[],
/** fetchUnhandledGroupRequestList 是否成功执行过;避免横幅显示 0 然后跳数字的闪烁 */
loaded: false
}),
@ -61,7 +54,7 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
/** 指定群下的未处理申请列表 */
getUnhandledGroupRequestListByGroupId:
(state) =>
(groupId: number): ImGroupRequestRespVO[] =>
(groupId: number): ImGroupRequestApi.GroupRequestRespVO[] =>
state.unhandledList.filter((r) => r.groupId === groupId)
},
@ -69,13 +62,13 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
/** 从 IndexedDB 恢复加群申请 */
async loadGroupRequestList(): Promise<boolean> {
try {
const cached = await getDb().getAll<GroupRequestDO>('groupRequests')
const cached = await getDb().getAll<ImGroupRequestApi.GroupRequestRespVO>('groupRequests')
if (!cached || cached.length === 0) {
return false
}
this.unhandledList = cached
.filter((request) => request.handleResult === ImGroupRequestHandleResult.UNHANDLED)
.sort((requestA, requestB) => requestB.id - requestA.id)
.toSorted((requestA, requestB) => requestB.id - requestA.id)
return true
} catch (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)
},
/** 保存单条加群申请 */
saveGroupRequest(request: ImGroupRequestRespVO): void {
saveGroupRequest(request: ImGroupRequestApi.GroupRequestRespVO): void {
void this.saveGroupRequestRecord(request).catch((error) =>
console.warn('[IM groupRequestStore] 本地加群申请写入失败', error)
)
@ -161,14 +154,14 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
*
* id /
*/
upsertGroupRequest(request: ImGroupRequestRespVO) {
upsertGroupRequest(request: ImGroupRequestApi.GroupRequestRespVO) {
void this.upsertGroupRequestForPull(request).catch((error) =>
console.warn('[IM groupRequestStore] 本地加群申请写入失败', error)
)
},
/** 本地合并 / 新增单条加群申请 */
async upsertGroupRequestForPull(request: ImGroupRequestRespVO): Promise<void> {
async upsertGroupRequestForPull(request: ImGroupRequestApi.GroupRequestRespVO): Promise<void> {
if (request.handleResult !== ImGroupRequestHandleResult.UNHANDLED) {
await this.removeGroupRequestByIdForPull(request.id)
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 { acceptHMRUpdate, defineStore } from 'pinia'
import {
getGroup as apiGetGroup,
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 { getGroup as apiGetGroup, getMyGroupList as apiGetMyGroupList } from '#/api/im/group'
import { getGroupMember as apiGetGroupMember, getGroupMemberList as apiGetGroupMemberList, updateGroupMember as apiUpdateGroupMember } from '#/api/im/group/member'
import { getCurrentUserId } from '#/views/im/utils/auth'
import {
@ -159,7 +153,7 @@ export const useGroupStore = defineStore('imGroupStore', {
return cachedGroup.members
}
try {
const cached = await getDb().getAllByIndex<GroupMemberDO>(
const cached = await getDb().getAllByIndex<GroupMember>(
'groupMembers',
'groupId',
groupId
@ -229,7 +223,7 @@ export const useGroupStore = defineStore('imGroupStore', {
return
}
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]))
this.groups = fresh.map((group) => {
const existing = groupMap.get(group.id)
@ -314,7 +308,7 @@ export const useGroupStore = defineStore('imGroupStore', {
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
return []
}
let meRaw: ImGroupMemberRespVO | undefined
let meRaw: ImGroupMemberApi.GroupMemberRespVO | undefined
const members = (list || []).map((member) => {
if (member.userId === requestUserId) {
meRaw = member
@ -903,7 +897,7 @@ export const useGroupStore = defineStore('imGroupStore', {
}
})
function convertGroup(group: ImGroupRespVO): Group {
function convertGroup(group: ImGroupApi.GroupRespVO): Group {
return {
id: group.id,
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(
message: NonNullable<ImGroupRespVO['pinnedMessages']>[number]
message: NonNullable<ImGroupApi.GroupRespVO['pinnedMessages']>[number]
): Message {
const currentUserId = getCurrentUserId()
return {
@ -940,7 +934,7 @@ function convertGroupMessageVO(
}
}
function convertGroupMember(member: ImGroupMemberRespVO, groupId: number): GroupMember {
function convertGroupMember(member: ImGroupMemberApi.GroupMemberRespVO, groupId: number): GroupMember {
return {
id: member.id,
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'
@ -74,7 +74,7 @@ export const useRtcStore = defineStore('imRtc', () => {
/** 当前阶段 */
const stage = ref<ImRtcCallStageValue>(ImRtcCallStage.IDLE)
/** 当前通话invite / accept / refreshToken 拿到的完整信息 */
const call = ref<ImRtcCallRespVO | null>(null)
const call = ref<ImRtcApi.RtcCallRespVO | null>(null)
/** 来电载荷;仅 INCOMING 阶段使用status 固定 INVITING其它字段 INVITE 专属 */
const incomingPayload = ref<ImRtcCallNotification | null>(null)
/** 进入 RUNNING 的时间戳用于通话时长展示reset 时清零 */
@ -115,13 +115,13 @@ export const useRtcStore = defineStore('imRtc', () => {
})
/** 私聊场景对端 userId自己是主叫则取首个 invitee否则取 inviter */
function resolvePrivatePeerUserId(c: ImRtcCallRespVO): number | undefined {
function resolvePrivatePeerUserId(c: ImRtcApi.RtcCallRespVO): number | undefined {
const myId = getCurrentUserId()
return c.inviterId === myId ? c.inviteeIds?.[0] : c.inviterId
}
/** 群活跃通话索引groupId -> 群通话摘要;用于群聊顶部胶囊条 */
const groupActiveCalls = ref<Map<number, ImRtcGroupCallRespVO>>(new Map())
const groupActiveCalls = ref<Map<number, ImRtcApi.RtcGroupCallRespVO>>(new Map())
/**
* 退 / pending
@ -147,7 +147,7 @@ export const useRtcStore = defineStore('imRtc', () => {
* RUNNING
* status RUNNING RUNNINGCREATED INVITING
*/
function startInviting(data: ImRtcCallRespVO) {
function startInviting(data: ImRtcApi.RtcCallRespVO) {
call.value = data
// 群通话场景写入本地胶囊条缓存
syncGroupActiveCall(data)
@ -184,7 +184,7 @@ export const useRtcStore = defineStore('imRtc', () => {
}
/** 进入通话中阶段 */
function enterRunning(data: ImRtcCallRespVO) {
function enterRunning(data: ImRtcApi.RtcCallRespVO) {
call.value = data
// 离开 INCOMING 阶段;清空来电载荷
incomingPayload.value = null
@ -252,7 +252,7 @@ export const useRtcStore = defineStore('imRtc', () => {
* LiveKit ParticipantConnected / Disconnected
* joinedUserIds / inviteeIds / getActiveCall
*/
function setGroupCall(payload: ImRtcGroupCallRespVO) {
function setGroupCall(payload: ImRtcApi.RtcGroupCallRespVO) {
if (!payload?.groupId) {
return
}
@ -267,7 +267,7 @@ export const useRtcStore = defineStore('imRtc', () => {
}
/** 两条群通话摘要内容相等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) {
return false
}
@ -294,7 +294,7 @@ export const useRtcStore = defineStore('imRtc', () => {
}
/** 获取群当前活跃通话;用于胶囊条按 groupId 查询 */
function getGroupCall(groupId: number): ImRtcGroupCallRespVO | undefined {
function getGroupCall(groupId: number): ImRtcApi.RtcGroupCallRespVO | undefined {
return groupActiveCalls.value.get(groupId)
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
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'
@ -31,12 +31,12 @@ function handleCreate() {
}
/** 编辑频道 */
function handleEdit(row: ImManagerChannelVO) {
function handleEdit(row: ImManagerChannelApi.Channel) {
formModalApi.setData(row).open()
}
/** 删除频道 */
async function handleDelete(row: ImManagerChannelVO) {
async function handleDelete(row: ImManagerChannelApi.Channel) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0
@ -77,7 +77,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true,
search: true
}
} as VxeTableGridOptions<ImManagerChannelVO>
} as VxeTableGridOptions<ImManagerChannelApi.Channel>
})
</script>

View File

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

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
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';
@ -34,12 +34,12 @@ function handleCreate() {
}
/** 编辑素材 */
function handleEdit(row: ImManagerChannelMaterialVO) {
function handleEdit(row: ImManagerChannelMaterialApi.Material) {
formModalApi.setData(row).open();
}
/** 删除素材 */
async function handleDelete(row: ImManagerChannelMaterialVO) {
async function handleDelete(row: ImManagerChannelMaterialApi.Material) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.title]),
duration: 0,
@ -80,7 +80,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true,
search: true,
},
} as VxeTableGridOptions<ImManagerChannelMaterialVO>,
} as VxeTableGridOptions<ImManagerChannelMaterialApi.Material>,
});
</script>

View File

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

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
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';
@ -34,7 +34,7 @@ function handleSend() {
}
/** 删除频道消息 */
async function handleDelete(row: ImManagerChannelMessageVO) {
async function handleDelete(row: ImManagerChannelMessageApi.ChannelMessage) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0,
@ -75,7 +75,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true,
search: true,
},
} as VxeTableGridOptions<ImManagerChannelMessageVO>,
} as VxeTableGridOptions<ImManagerChannelMessageApi.ChannelMessage>,
});
</script>

View File

@ -1,5 +1,5 @@
<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';
@ -29,7 +29,7 @@ const emit = defineEmits<{
}>();
const loading = ref(false);
const channelList = ref<ImManagerChannelVO[]>([]);
const channelList = ref<ImManagerChannelApi.Channel[]>([]);
const value = computed({
get: () => props.modelValue,

View File

@ -1,7 +1,7 @@
<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';
@ -29,7 +29,7 @@ const emit = defineEmits<{
}>();
const loading = ref(false);
const groupList = ref<ImManagerGroupVO[]>([]);
const groupList = ref<ImManagerGroupApi.Group[]>([]);
const value = computed({
get: () => props.modelValue,
@ -57,7 +57,7 @@ async function loadGroupList() {
}
}
watch(() => props.modelValue, loadGroupList, { immediate: true });
onMounted(loadGroupList);
</script>
<template>

View File

@ -1,5 +1,5 @@
<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';
@ -29,7 +29,7 @@ const emit = defineEmits<{
}>();
const loading = ref(false);
const materialList = ref<ImManagerChannelMaterialVO[]>([]);
const materialList = ref<ImManagerChannelMaterialApi.Material[]>([]);
const value = computed({
get: () => props.modelValue,

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<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';
@ -19,7 +19,7 @@ import { probeImageSize } from '#/views/im/manager/utils/format';
import { useItemFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<ImManagerFacePackItemVO>();
const formData = ref<ImManagerFacePackItemApi.FacePackItem>();
const packId = ref(0);
const lastUrl = ref('');
const getTitle = computed(() => {
@ -41,7 +41,13 @@ const [Form, formApi] = useVbenForm({
/** 回填图片尺寸 */
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;
}
lastUrl.value = url;
@ -68,7 +74,7 @@ const [Modal, modalApi] = useVbenModal({
return;
}
modalApi.lock();
const data = (await formApi.getValues()) as ImManagerFacePackItemVO;
const data = (await formApi.getValues()) as ImManagerFacePackItemApi.FacePackItem;
try {
data.packId = data.packId || packId.value;
await (formData.value?.id

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
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';
@ -24,7 +24,7 @@ function handleRefresh() {
}
/** 删除用户表情 */
async function handleDelete(row: ImManagerFaceUserItemVO) {
async function handleDelete(row: ImManagerFaceUserItemApi.FaceUserItem) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name || row.id]),
duration: 0,
@ -65,7 +65,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true,
search: true,
},
} as VxeTableGridOptions<ImManagerFaceUserItemVO>,
} as VxeTableGridOptions<ImManagerFaceUserItemApi.FaceUserItem>,
});
</script>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
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';
@ -17,7 +17,7 @@ defineOptions({ name: 'ImManagerFriend' });
const router = useRouter();
/** 查看私聊消息 */
function handleConversation(row: ImManagerFriendVO) {
function handleConversation(row: ImManagerFriendApi.Friend) {
router.push({
name: 'ImPrivateMessage',
query: {
@ -54,7 +54,7 @@ const [Grid] = useVbenVxeGrid({
refresh: true,
search: true,
},
} as VxeTableGridOptions<ImManagerFriendVO>,
} as VxeTableGridOptions<ImManagerFriendApi.Friend>,
});
</script>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
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';
@ -42,7 +42,7 @@ const [Grid] = useVbenVxeGrid({
refresh: true,
search: true,
},
} as VxeTableGridOptions<ImManagerFriendRequestVO>,
} as VxeTableGridOptions<ImManagerFriendRequestApi.FriendRequest>,
});
</script>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
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 { useRouter } from 'vue-router';
@ -39,12 +39,12 @@ function handleRefresh() {
}
/** 打开详情 */
function handleDetail(row: ImManagerGroupVO) {
function handleDetail(row: ImManagerGroupApi.Group) {
detailRef.value?.open(row);
}
/** 查看群聊消息 */
function handleConversation(row: ImManagerGroupVO) {
function handleConversation(row: ImManagerGroupApi.Group) {
router.push({
name: 'ImGroupMessage',
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();
}
/** 解封群 */
async function handleUnban(row: ImManagerGroupVO) {
async function handleUnban(row: ImManagerGroupApi.Group) {
await confirm(`确认解封群「${row.name}」吗?`);
await unbanManagerGroup(row.id);
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 dissolveManagerGroup(row.id);
message.success('解散成功');
@ -99,7 +99,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true,
search: true,
},
} as VxeTableGridOptions<ImManagerGroupVO>,
} as VxeTableGridOptions<ImManagerGroupApi.Group>,
});
</script>

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