feat(im):规范 Vben IM 组件目录并修复聊天端迁移问题
文件命名与目录整理: - IM home/manager 组件文件统一 PascalCase → kebab-case,并新增各级 components/index.ts barrel 导出 - manager 选择器按业务模块就近收敛到频道、素材、群组目录,删除根 components 下的重复实现 - UserMultiSelect 改为复用 system/user/components/UserSelect,并补充多选与 getUserList 回显能力 - 合并 statistics 子组件导出,MessageContentPreview 调整为 content-preview 问题修复: - 群聊发送按钮由 Element Plus split-button 写法改为 antd DropdownButton,恢复「发送回执消息」入口 - 修复 scoped 下暗色模式选择器塌缩导致整页发红的问题 - 修复会话「+」菜单图标与文字折行问题 - 修复推荐名片、转发、添加好友弹窗冒出多余 antd 默认底栏的问题 代码规范: - 清理 IM 模块类型别名、注释和工具方法写法,保持 Vben 规范 - constants.ts 内容类型判定集合由数组改为 Set - 优化 message/image/pull 等工具函数的 lint 写法pull/367/head
parent
24813f00f5
commit
2cbec901e1
|
|
@ -41,6 +41,13 @@ export function getUser(id: number) {
|
||||||
return requestClient.get<SystemUserApi.User>(`/system/user/get?id=${id}`);
|
return requestClient.get<SystemUserApi.User>(`/system/user/get?id=${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 查询用户列表 */
|
||||||
|
export function getUserList(ids: number[]) {
|
||||||
|
return requestClient.get<SystemUserApi.User[]>('/system/user/list', {
|
||||||
|
params: { ids: ids.join(',') },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 新增用户 */
|
/** 新增用户 */
|
||||||
export function createUser(data: SystemUserApi.User) {
|
export function createUser(data: SystemUserApi.User) {
|
||||||
return requestClient.post('/system/user/create', data);
|
return requestClient.post('/system/user/create', data);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
getCardLabelInfo
|
getCardLabelInfo
|
||||||
} from '#/views/im/utils/message'
|
} from '#/views/im/utils/message'
|
||||||
|
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImCardBubble' })
|
defineOptions({ name: 'ImCardBubble' })
|
||||||
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as CardBubble } from './card-bubble.vue';
|
||||||
|
export { default as CardLineLabel } from './card-line-label.vue';
|
||||||
|
|
@ -14,7 +14,7 @@ import { getCurrentUserId } from '#/views/im/utils/auth'
|
||||||
import { ImFriendAddSource } from '../../../utils/constants'
|
import { ImFriendAddSource } from '../../../utils/constants'
|
||||||
import { getGenderColor, getGenderIcon } from '../../../utils/user'
|
import { getGenderColor, getGenderIcon } from '../../../utils/user'
|
||||||
import { useFriendStore } from '../../store/friendStore'
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImFriendAddDialog' })
|
defineOptions({ name: 'ImFriendAddDialog' })
|
||||||
|
|
||||||
|
|
@ -159,7 +159,13 @@ async function handleSubmitApply() {
|
||||||
- 第一层 search:按昵称搜索用户列表
|
- 第一层 search:按昵称搜索用户列表
|
||||||
- 第二层 apply:选中用户后展开「申请添加朋友」表单(申请理由 + 备注)
|
- 第二层 apply:选中用户后展开「申请添加朋友」表单(申请理由 + 备注)
|
||||||
-->
|
-->
|
||||||
<Modal v-model:open="visible" :title="dialogTitle" width="480px" :mask-closable="false">
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="480px"
|
||||||
|
:mask-closable="false"
|
||||||
|
:footer="step === 'apply' ? undefined : null"
|
||||||
|
>
|
||||||
<!-- 第一层:搜索 + 用户列表 -->
|
<!-- 第一层:搜索 + 用户列表 -->
|
||||||
<template v-if="step === 'search'">
|
<template v-if="step === 'search'">
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import type { FriendLite } from '../../types'
|
import type { FriendLite } from '../../types'
|
||||||
|
|
||||||
import { useImUiStore } from '../../store/uiStore'
|
import { useImUiStore } from '../../store/uiStore'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImFriendItem' })
|
defineOptions({ name: 'ImFriendItem' })
|
||||||
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as FriendAddDialog } from './friend-add-dialog.vue';
|
||||||
|
export { default as FriendItem } from './friend-item.vue';
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupMemberLite } from './GroupMember.vue'
|
import type { GroupMemberLite } from './group-member.vue'
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Button, message, Modal } from 'ant-design-vue'
|
||||||
import { addGroupAdmin, removeGroupAdmin } from '#/api/im/group'
|
import { addGroupAdmin, removeGroupAdmin } from '#/api/im/group'
|
||||||
import { GROUP_ADMIN_MAX_COUNT } from '#/views/im/utils/config'
|
import { GROUP_ADMIN_MAX_COUNT } from '#/views/im/utils/config'
|
||||||
|
|
||||||
import GroupMemberPickerPanel from '../picker/GroupMemberPickerPanel.vue'
|
import { GroupMemberPickerPanel } from '../picker'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupAdminSetDialog' })
|
defineOptions({ name: 'ImGroupAdminSetDialog' })
|
||||||
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
import { getMemberDisplayName } from '../../../utils/user'
|
import { getMemberDisplayName } from '../../../utils/user'
|
||||||
import { useFriendStore } from '../../store/friendStore'
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
import { useGroupStore } from '../../store/groupStore'
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupAvatar' })
|
defineOptions({ name: 'ImGroupAvatar' })
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { createGroup } from '#/api/im/group'
|
||||||
import { buildDefaultGroupName } from '../../../utils/group'
|
import { buildDefaultGroupName } from '../../../utils/group'
|
||||||
import { useFriendStore } from '../../store/friendStore'
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
import { useGroupStore } from '../../store/groupStore'
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
import FriendPickerPanel from '../picker/FriendPickerPanel.vue'
|
import { FriendPickerPanel } from '../picker'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupCreateDialog' })
|
defineOptions({ name: 'ImGroupCreateDialog' })
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { getGroupDisplayName } from '../../../utils/user'
|
||||||
import { useConversationStore } from '../../store/conversationStore'
|
import { useConversationStore } from '../../store/conversationStore'
|
||||||
import { useGroupStore } from '../../store/groupStore'
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
import { useImUiStore } from '../../store/uiStore'
|
import { useImUiStore } from '../../store/uiStore'
|
||||||
import GroupInfo from './GroupInfo.vue'
|
import GroupInfo from './group-info.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupInfoCard' })
|
defineOptions({ name: 'ImGroupInfoCard' })
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Friend, GroupLite, GroupMember } from '../../types'
|
import type { Friend, GroupLite, GroupMember } from '../../types'
|
||||||
import type { GroupMemberLite } from './GroupMember.vue'
|
import type { GroupMemberLite } from './group-member.vue'
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
|
@ -13,8 +13,8 @@ import { getCurrentUserId } from '#/views/im/utils/auth'
|
||||||
import { getMemberDisplayName, isGroupQuit } from '../../../utils/user'
|
import { getMemberDisplayName, isGroupQuit } from '../../../utils/user'
|
||||||
import { useFriendStore } from '../../store/friendStore'
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
import { useGroupStore } from '../../store/groupStore'
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
import GroupAvatar from './GroupAvatar.vue'
|
import GroupAvatar from './group-avatar.vue'
|
||||||
import GroupMemberGrid from './GroupMemberGrid.vue'
|
import GroupMemberGrid from './group-member-grid.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupInfo' })
|
defineOptions({ name: 'ImGroupInfo' })
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupLite } from '../../types'
|
import type { GroupLite } from '../../types'
|
||||||
|
|
||||||
import GroupAvatar from './GroupAvatar.vue'
|
import GroupAvatar from './group-avatar.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupItem' })
|
defineOptions({ name: 'ImGroupItem' })
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupMemberLite } from './GroupMember.vue'
|
import type { GroupMemberLite } from './group-member.vue'
|
||||||
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { ImGroupMemberRole } from '#/views/im/utils/constants'
|
||||||
|
|
||||||
import { useFriendStore } from '../../store/friendStore'
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
import { useGroupStore } from '../../store/groupStore'
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
import FriendPickerPanel from '../picker/FriendPickerPanel.vue'
|
import { FriendPickerPanel } from '../picker'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupMemberAddDialog' })
|
defineOptions({ name: 'ImGroupMemberAddDialog' })
|
||||||
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupMemberLite } from './GroupMember.vue'
|
import type { GroupMemberLite } from './group-member.vue'
|
||||||
|
|
||||||
import { ImFriendAddSource } from '../../../utils/constants'
|
import { ImFriendAddSource } from '../../../utils/constants'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupMemberGrid' })
|
defineOptions({ name: 'ImGroupMemberGrid' })
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupMemberLite } from './GroupMember.vue'
|
import type { GroupMemberLite } from './group-member.vue'
|
||||||
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { DICT_TYPE } from '@vben/constants'
|
||||||
import { getDictLabel } from '@vben/hooks'
|
import { getDictLabel } from '@vben/hooks'
|
||||||
|
|
||||||
import { ImGroupMemberRole } from '../../../utils/constants'
|
import { ImGroupMemberRole } from '../../../utils/constants'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupMemberItem' })
|
defineOptions({ name: 'ImGroupMemberItem' })
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupMemberLite } from './GroupMember.vue'
|
import type { GroupMemberLite } from './group-member.vue'
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Button, message, Modal } from 'ant-design-vue'
|
||||||
|
|
||||||
import { removeGroupMember } from '#/api/im/group/member'
|
import { removeGroupMember } from '#/api/im/group/member'
|
||||||
|
|
||||||
import GroupMemberPickerPanel from '../picker/GroupMemberPickerPanel.vue'
|
import { GroupMemberPickerPanel } from '../picker'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupMemberRemoveDialog' })
|
defineOptions({ name: 'ImGroupMemberRemoveDialog' })
|
||||||
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { ImFriendAddSource } from '../../../utils/constants'
|
import { ImFriendAddSource } from '../../../utils/constants'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupMember' })
|
defineOptions({ name: 'ImGroupMember' })
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupMemberLite } from './GroupMember.vue'
|
import type { GroupMemberLite } from './group-member.vue'
|
||||||
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { Button, message, Modal } from 'ant-design-vue'
|
||||||
|
|
||||||
import { transferGroupOwner } from '#/api/im/group'
|
import { transferGroupOwner } from '#/api/im/group'
|
||||||
|
|
||||||
import GroupMemberPickerPanel from '../picker/GroupMemberPickerPanel.vue'
|
import { GroupMemberPickerPanel } from '../picker'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupOwnerTransferDialog' })
|
defineOptions({ name: 'ImGroupOwnerTransferDialog' })
|
||||||
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { getGroupRequestListByGroupId } from '#/api/im/group/request'
|
||||||
import { ImGroupRequestHandleResult } from '#/views/im/utils/constants'
|
import { ImGroupRequestHandleResult } from '#/views/im/utils/constants'
|
||||||
|
|
||||||
import { useGroupRequestStore } from '../../store/groupRequestStore'
|
import { useGroupRequestStore } from '../../store/groupRequestStore'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupRequestListDialog' })
|
defineOptions({ name: 'ImGroupRequestListDialog' })
|
||||||
|
|
||||||
|
|
@ -166,6 +166,7 @@ function updateLocalResult(id: number, handleResult: number) {
|
||||||
v-model:open="visible"
|
v-model:open="visible"
|
||||||
title="进群申请"
|
title="进群申请"
|
||||||
width="560px"
|
width="560px"
|
||||||
|
:footer="null"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
class="im-group-request-list__dialog"
|
class="im-group-request-list__dialog"
|
||||||
>
|
>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
export { default as GroupAdminSetDialog } from './group-admin-set-dialog.vue';
|
||||||
|
export { default as GroupAvatar } from './group-avatar.vue';
|
||||||
|
export { default as GroupCreateDialog } from './group-create-dialog.vue';
|
||||||
|
export { default as GroupInfoCard } from './group-info-card.vue';
|
||||||
|
export { default as GroupInfo } from './group-info.vue';
|
||||||
|
export { default as GroupItem } from './group-item.vue';
|
||||||
|
export { default as GroupMemberAddDialog } from './group-member-add-dialog.vue';
|
||||||
|
export { default as GroupMemberGrid } from './group-member-grid.vue';
|
||||||
|
export { default as GroupMemberItem } from './group-member-item.vue';
|
||||||
|
export { default as GroupMemberRemoveDialog } from './group-member-remove-dialog.vue';
|
||||||
|
export { default as GroupMember } from './group-member.vue';
|
||||||
|
export type { GroupMemberLite } from './group-member.vue';
|
||||||
|
export { default as GroupMuteMemberDialog } from './group-mute-member-dialog.vue';
|
||||||
|
export { default as GroupOwnerTransferDialog } from './group-owner-transfer-dialog.vue';
|
||||||
|
export { default as GroupRequestListDialog } from './group-request-list-dialog.vue';
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { default as ContextMenu } from './context-menu.vue';
|
||||||
|
export { default as PagedScroller } from './paged-scroller.vue';
|
||||||
|
export { default as ResizableAside } from './resizable-aside.vue';
|
||||||
|
export { default as ToolBar } from './tool-bar.vue';
|
||||||
|
|
@ -11,6 +11,7 @@ const props = withDefaults(
|
||||||
threshold?: number // 距底多少 px 触发下一页
|
threshold?: number // 距底多少 px 触发下一页
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
|
itemKey: undefined,
|
||||||
pageSize: 30,
|
pageSize: 30,
|
||||||
threshold: 30
|
threshold: 30
|
||||||
}
|
}
|
||||||
|
|
@ -9,8 +9,8 @@ import { Input, message } from 'ant-design-vue'
|
||||||
|
|
||||||
import { ImConversationType } from '../../../utils/constants'
|
import { ImConversationType } from '../../../utils/constants'
|
||||||
import { filterConversationsByKeyword, getConversationKey } from '../../../utils/conversation'
|
import { filterConversationsByKeyword, getConversationKey } from '../../../utils/conversation'
|
||||||
import GroupAvatar from '../group/GroupAvatar.vue'
|
import { GroupAvatar } from '../group'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImConversationPickerPanel' })
|
defineOptions({ name: 'ImConversationPickerPanel' })
|
||||||
|
|
||||||
|
|
@ -7,10 +7,10 @@ import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
|
|
||||||
import { Input, message } from 'ant-design-vue'
|
import { Input, message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
import { PagedScroller } from '..'
|
||||||
import { useFriendBuckets } from '../../composables/useFriendBuckets'
|
import { useFriendBuckets } from '../../composables/useFriendBuckets'
|
||||||
import { useSelectedItems } from '../../composables/useSelectedItems'
|
import { useSelectedItems } from '../../composables/useSelectedItems'
|
||||||
import PagedScroller from '../PagedScroller.vue'
|
import { UserAvatar } from '../user'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ImFriendPickerPanel' })
|
defineOptions({ name: 'ImFriendPickerPanel' })
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupMemberLite } from '../group/GroupMember.vue'
|
import type { GroupMemberLite } from '../group'
|
||||||
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
|
@ -8,11 +8,10 @@ import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
|
|
||||||
import { Input, message } from 'ant-design-vue'
|
import { Input, message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
import { PagedScroller } from '..'
|
||||||
import { useSelectedItems } from '../../composables/useSelectedItems'
|
import { useSelectedItems } from '../../composables/useSelectedItems'
|
||||||
import GroupMemberGrid from '../group/GroupMemberGrid.vue'
|
import { GroupMemberGrid, GroupMemberItem } from '../group'
|
||||||
import GroupMemberItem from '../group/GroupMemberItem.vue'
|
import { UserAvatar } from '../user'
|
||||||
import PagedScroller from '../PagedScroller.vue'
|
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupMemberPickerPanel' })
|
defineOptions({ name: 'ImGroupMemberPickerPanel' })
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as ConversationPickerPanel } from './conversation-picker-panel.vue';
|
||||||
|
export { default as FriendPickerPanel } from './friend-picker-panel.vue';
|
||||||
|
export { default as GroupMemberPickerPanel } from './group-member-picker-panel.vue';
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export { default as RtcCallContainer } from './rtc-call-container.vue';
|
||||||
|
export { default as RtcCallIncoming } from './rtc-call-incoming.vue';
|
||||||
|
export { default as RtcCallInviting } from './rtc-call-inviting.vue';
|
||||||
|
export { default as RtcCallMemberPickerDialog } from './rtc-call-member-picker-dialog.vue';
|
||||||
|
export { default as RtcCallParticipantTile } from './rtc-call-participant-tile.vue';
|
||||||
|
export type { CallParticipantVM } from './rtc-call-participant-tile.vue';
|
||||||
|
export { default as RtcCallRunning } from './rtc-call-running.vue';
|
||||||
|
export { default as RtcGroupCallBanner } from './rtc-group-call-banner.vue';
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { CallParticipantVM } from './RtcCallParticipantTile.vue'
|
import type { CallParticipantVM } from './rtc-call-participant-tile.vue'
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
|
@ -26,10 +26,10 @@ import { getSenderAvatar, getSenderDisplayName } from '#/views/im/utils/user'
|
||||||
|
|
||||||
import { useLiveKitRoom } from '../../composables/useLiveKitRoom'
|
import { useLiveKitRoom } from '../../composables/useLiveKitRoom'
|
||||||
import { useRtcStore } from '../../store/rtcStore'
|
import { useRtcStore } from '../../store/rtcStore'
|
||||||
import RtcCallIncoming from './RtcCallIncoming.vue'
|
import RtcCallIncoming from './rtc-call-incoming.vue'
|
||||||
import RtcCallInviting from './RtcCallInviting.vue'
|
import RtcCallInviting from './rtc-call-inviting.vue'
|
||||||
import RtcCallMemberPickerDialog from './RtcCallMemberPickerDialog.vue'
|
import RtcCallMemberPickerDialog from './rtc-call-member-picker-dialog.vue'
|
||||||
import RtcCallRunning from './RtcCallRunning.vue'
|
import RtcCallRunning from './rtc-call-running.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImRtcCallContainer' })
|
defineOptions({ name: 'ImRtcCallContainer' })
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { getDictLabel } from '@vben/hooks'
|
||||||
import { IconifyIcon as Icon } from '@vben/icons'
|
import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
|
|
||||||
import { useGroupCallMembers } from '../../composables/useGroupCallMembers'
|
import { useGroupCallMembers } from '../../composables/useGroupCallMembers'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
accepting?: boolean
|
accepting?: boolean
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { IconifyIcon as Icon } from '@vben/icons'
|
import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
|
|
||||||
import { useMediaStreamElement } from '../../composables/useMediaStreamElement'
|
import { useMediaStreamElement } from '../../composables/useMediaStreamElement'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
cameraEnabled: boolean
|
cameraEnabled: boolean
|
||||||
|
|
@ -17,9 +17,9 @@ const props = defineProps<{
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
cancel: []
|
cancel: []
|
||||||
'toggle-camera': []
|
toggleCamera: []
|
||||||
'toggle-mic': []
|
toggleMic: []
|
||||||
'toggle-speaker': []
|
toggleSpeaker: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const localVideoRef = useMediaStreamElement<HTMLVideoElement>(() => props.localStream)
|
const localVideoRef = useMediaStreamElement<HTMLVideoElement>(() => props.localStream)
|
||||||
|
|
@ -61,7 +61,7 @@ const localVideoRef = useMediaStreamElement<HTMLVideoElement>(() => props.localS
|
||||||
<div class="flex flex-shrink-0 gap-4 justify-around items-center pt-4 px-5 pb-5">
|
<div class="flex flex-shrink-0 gap-4 justify-around items-center pt-4 px-5 pb-5">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-2 items-center cursor-pointer select-none"
|
class="flex flex-col gap-2 items-center cursor-pointer select-none"
|
||||||
@click="$emit('toggle-mic')"
|
@click="$emit('toggleMic')"
|
||||||
>
|
>
|
||||||
<!-- ant-design 系列里 mic 有 audio-muted-outlined 变体;speaker / camera 没有 muted 变体,off 态借 tabler:*-off 表达斜线 -->
|
<!-- ant-design 系列里 mic 有 audio-muted-outlined 变体;speaker / camera 没有 muted 变体,off 态借 tabler:*-off 表达斜线 -->
|
||||||
<span
|
<span
|
||||||
|
|
@ -91,7 +91,7 @@ const localVideoRef = useMediaStreamElement<HTMLVideoElement>(() => props.localS
|
||||||
<div
|
<div
|
||||||
v-if="isVideo"
|
v-if="isVideo"
|
||||||
class="flex flex-col gap-2 items-center cursor-pointer select-none"
|
class="flex flex-col gap-2 items-center cursor-pointer select-none"
|
||||||
@click="$emit('toggle-camera')"
|
@click="$emit('toggleCamera')"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex justify-center items-center w-12 h-12 rounded-full"
|
class="flex justify-center items-center w-12 h-12 rounded-full"
|
||||||
|
|
@ -109,7 +109,7 @@ const localVideoRef = useMediaStreamElement<HTMLVideoElement>(() => props.localS
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex flex-col gap-2 items-center cursor-pointer select-none"
|
class="flex flex-col gap-2 items-center cursor-pointer select-none"
|
||||||
@click="$emit('toggle-speaker')"
|
@click="$emit('toggleSpeaker')"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex justify-center items-center w-12 h-12 rounded-full"
|
class="flex justify-center items-center w-12 h-12 rounded-full"
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupMemberLite } from '../group/GroupMember.vue'
|
import type { GroupMemberLite } from '../group'
|
||||||
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Button, Modal } from 'ant-design-vue'
|
||||||
import { getCurrentUserId } from '#/views/im/utils/auth'
|
import { getCurrentUserId } from '#/views/im/utils/auth'
|
||||||
|
|
||||||
import { useGroupStore } from '../../store/groupStore'
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
import GroupMemberPickerPanel from '../picker/GroupMemberPickerPanel.vue'
|
import { GroupMemberPickerPanel } from '../picker'
|
||||||
|
|
||||||
defineOptions({ name: 'ImRtcCallMemberPickerDialog' })
|
defineOptions({ name: 'ImRtcCallMemberPickerDialog' })
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useMediaStreamElement } from '../../composables/useMediaStreamElement'
|
import { useMediaStreamElement } from '../../composables/useMediaStreamElement'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
export interface CallParticipantVM {
|
export interface CallParticipantVM {
|
||||||
userId: number
|
userId: number
|
||||||
|
|
@ -6,8 +6,8 @@ import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
import { formatCallDuration } from '#/views/im/utils/time'
|
import { formatCallDuration } from '#/views/im/utils/time'
|
||||||
|
|
||||||
import { useMediaStreamElement } from '../../composables/useMediaStreamElement'
|
import { useMediaStreamElement } from '../../composables/useMediaStreamElement'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
import RtcCallParticipantTile, { type CallParticipantVM } from './RtcCallParticipantTile.vue'
|
import RtcCallParticipantTile, { type CallParticipantVM } from './rtc-call-participant-tile.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
cameraEnabled: boolean
|
cameraEnabled: boolean
|
||||||
|
|
@ -34,12 +34,12 @@ const props = defineProps<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
'add-member': []
|
addMember: []
|
||||||
hangup: []
|
hangup: []
|
||||||
'toggle-camera': []
|
toggleCamera: []
|
||||||
'toggle-mic': []
|
toggleMic: []
|
||||||
'toggle-screen-share': []
|
toggleScreenShare: []
|
||||||
'toggle-speaker': []
|
toggleSpeaker: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
/** 网格列数;按人数自适应;返回 UnoCSS class 字面量让 JIT 扫描器静态识别 */
|
/** 网格列数;按人数自适应;返回 UnoCSS class 字面量让 JIT 扫描器静态识别 */
|
||||||
|
|
@ -180,7 +180,7 @@ const formattedDuration = computed(() =>
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
||||||
@click="$emit('toggle-mic')"
|
@click="$emit('toggleMic')"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex justify-center items-center w-[52px] h-[52px] rounded-full"
|
class="flex justify-center items-center w-[52px] h-[52px] rounded-full"
|
||||||
|
|
@ -197,7 +197,7 @@ const formattedDuration = computed(() =>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
||||||
@click="$emit('toggle-speaker')"
|
@click="$emit('toggleSpeaker')"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex justify-center items-center w-[52px] h-[52px] rounded-full"
|
class="flex justify-center items-center w-[52px] h-[52px] rounded-full"
|
||||||
|
|
@ -216,7 +216,7 @@ const formattedDuration = computed(() =>
|
||||||
<div
|
<div
|
||||||
v-if="isVideo || isGroup"
|
v-if="isVideo || isGroup"
|
||||||
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
||||||
@click="$emit('toggle-camera')"
|
@click="$emit('toggleCamera')"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex justify-center items-center w-[52px] h-[52px] rounded-full"
|
class="flex justify-center items-center w-[52px] h-[52px] rounded-full"
|
||||||
|
|
@ -235,7 +235,7 @@ const formattedDuration = computed(() =>
|
||||||
<template v-if="isGroup">
|
<template v-if="isGroup">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
||||||
@click="$emit('toggle-screen-share')"
|
@click="$emit('toggleScreenShare')"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex justify-center items-center w-[52px] h-[52px] rounded-full"
|
class="flex justify-center items-center w-[52px] h-[52px] rounded-full"
|
||||||
|
|
@ -252,7 +252,7 @@ const formattedDuration = computed(() =>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
||||||
@click="$emit('add-member')"
|
@click="$emit('addMember')"
|
||||||
>
|
>
|
||||||
<span class="flex justify-center items-center w-[52px] h-[52px] text-white rounded-full bg-white/15">
|
<span class="flex justify-center items-center w-[52px] h-[52px] text-white rounded-full bg-white/15">
|
||||||
<Icon icon="ant-design:plus-outlined" :size="22" />
|
<Icon icon="ant-design:plus-outlined" :size="22" />
|
||||||
|
|
@ -12,7 +12,7 @@ import { getCurrentUserId } from '#/views/im/utils/auth'
|
||||||
|
|
||||||
import { useGroupCallMembers } from '../../composables/useGroupCallMembers'
|
import { useGroupCallMembers } from '../../composables/useGroupCallMembers'
|
||||||
import { useRtcStore } from '../../store/rtcStore'
|
import { useRtcStore } from '../../store/rtcStore'
|
||||||
import UserAvatar from '../user/UserAvatar.vue'
|
import { UserAvatar } from '../user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImRtcGroupCallBanner' })
|
defineOptions({ name: 'ImRtcGroupCallBanner' })
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { Badge } from 'ant-design-vue'
|
||||||
import { useConversationStore } from '../store/conversationStore'
|
import { useConversationStore } from '../store/conversationStore'
|
||||||
import { useFriendStore } from '../store/friendStore'
|
import { useFriendStore } from '../store/friendStore'
|
||||||
import { useImUiStore } from '../store/uiStore'
|
import { useImUiStore } from '../store/uiStore'
|
||||||
import UserAvatar from './user/UserAvatar.vue'
|
import { UserAvatar } from './user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImToolBar' })
|
defineOptions({ name: 'ImToolBar' })
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export { default as RecommendCardDialog } from './recommend-card-dialog.vue';
|
||||||
|
export { default as UserAvatar } from './user-avatar.vue';
|
||||||
|
export { default as UserInfoCard } from './user-info-card.vue';
|
||||||
|
export { default as UserInfo } from './user-info.vue';
|
||||||
|
export type { UserInfoRelation } from './user-info.vue';
|
||||||
|
|
@ -8,7 +8,7 @@ import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
import { Button, Input, message, Modal } from 'ant-design-vue'
|
import { Button, Input, message, Modal } from 'ant-design-vue'
|
||||||
|
|
||||||
import { createGroup } from '#/api/im/group'
|
import { createGroup } from '#/api/im/group'
|
||||||
import CardBubble from '#/views/im/home/components/card/CardBubble.vue'
|
import { CardBubble } from '#/views/im/home/components/card'
|
||||||
|
|
||||||
import { ImContentType, ImConversationType, isGroupConversation } from '../../../utils/constants'
|
import { ImContentType, ImConversationType, isGroupConversation } from '../../../utils/constants'
|
||||||
import { getConversationKey } from '../../../utils/conversation'
|
import { getConversationKey } from '../../../utils/conversation'
|
||||||
|
|
@ -16,12 +16,12 @@ import { buildDefaultGroupName } from '../../../utils/group'
|
||||||
import { type CardTarget, serializeMessage } from '../../../utils/message'
|
import { type CardTarget, serializeMessage } from '../../../utils/message'
|
||||||
import { isGroupQuit } from '../../../utils/user'
|
import { isGroupQuit } from '../../../utils/user'
|
||||||
import { useMessageSender } from '../../composables/useMessageSender'
|
import { useMessageSender } from '../../composables/useMessageSender'
|
||||||
import FacePicker from '../../pages/conversation/components/input/FacePicker.vue'
|
import { FacePicker } from '../../pages/conversation/components/input'
|
||||||
import { useConversationStore } from '../../store/conversationStore'
|
import { useConversationStore } from '../../store/conversationStore'
|
||||||
import { useFriendStore } from '../../store/friendStore'
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
import { useGroupStore } from '../../store/groupStore'
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
import ConversationPickerPanel from '../picker/ConversationPickerPanel.vue'
|
import { ConversationPickerPanel } from '../picker'
|
||||||
import FriendPickerPanel from '../picker/FriendPickerPanel.vue'
|
import { FriendPickerPanel } from '../picker'
|
||||||
|
|
||||||
defineOptions({ name: 'ImRecommendCardDialog' })
|
defineOptions({ name: 'ImRecommendCardDialog' })
|
||||||
|
|
||||||
|
|
@ -222,6 +222,7 @@ async function handleCreateGroupAndSend() {
|
||||||
v-model:open="visible"
|
v-model:open="visible"
|
||||||
width="720px"
|
width="720px"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
|
:footer="view === 'conversation' ? null : undefined"
|
||||||
class="im-picker-dialog im-recommend-dialog"
|
class="im-picker-dialog im-recommend-dialog"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
|
|
@ -26,11 +26,16 @@ const props = withDefaults(
|
||||||
user?: User // 额外的用户信息,传了点击就不用现拉接口(弹名片用)
|
user?: User // 额外的用户信息,传了点击就不用现拉接口(弹名片用)
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
size: 42,
|
addSourceExtra: undefined,
|
||||||
radius: '15%',
|
|
||||||
clickable: true,
|
clickable: true,
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
previewable: false,
|
previewable: false,
|
||||||
previewZIndex: 2000,
|
previewZIndex: 2000,
|
||||||
|
radius: '15%',
|
||||||
|
size: 42,
|
||||||
|
url: undefined,
|
||||||
|
user: undefined,
|
||||||
addSource: ImFriendAddSource.SEARCH
|
addSource: ImFriendAddSource.SEARCH
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -9,7 +9,7 @@ import { getFriendDisplayName } from '../../../utils/user'
|
||||||
import { useConversationStore } from '../../store/conversationStore'
|
import { useConversationStore } from '../../store/conversationStore'
|
||||||
import { useFriendStore } from '../../store/friendStore'
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
import { useImUiStore } from '../../store/uiStore'
|
import { useImUiStore } from '../../store/uiStore'
|
||||||
import UserInfo, { type UserInfoRelation } from './UserInfo.vue'
|
import UserInfo, { type UserInfoRelation } from './user-info.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImUserInfoCard' })
|
defineOptions({ name: 'ImUserInfoCard' })
|
||||||
|
|
||||||
|
|
@ -17,9 +17,9 @@ import { ImFriendAddSource } from '../../../utils/constants'
|
||||||
import { toUserCardTarget } from '../../../utils/message'
|
import { toUserCardTarget } from '../../../utils/message'
|
||||||
import { getGenderColor, getGenderIcon } from '../../../utils/user'
|
import { getGenderColor, getGenderIcon } from '../../../utils/user'
|
||||||
import { useFriendStore } from '../../store/friendStore'
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
import FriendAddDialog from '../friend/FriendAddDialog.vue'
|
import { FriendAddDialog } from '../friend'
|
||||||
import RecommendCardDialog from './RecommendCardDialog.vue'
|
import RecommendCardDialog from './recommend-card-dialog.vue'
|
||||||
import UserAvatar from './UserAvatar.vue'
|
import UserAvatar from './user-avatar.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImUserInfo' })
|
defineOptions({ name: 'ImUserInfo' })
|
||||||
|
|
||||||
|
|
@ -73,12 +73,11 @@ export function useFriendBuckets(
|
||||||
const map = new Map<string, FriendLite[]>()
|
const map = new Map<string, FriendLite[]>()
|
||||||
for (const friend of filtered.value) {
|
for (const friend of filtered.value) {
|
||||||
const letter = getBucketLetter(friend)
|
const letter = getBucketLetter(friend)
|
||||||
if (!map.has(letter)) {
|
const bucket = map.get(letter) ?? []
|
||||||
map.set(letter, [])
|
bucket.push(friend)
|
||||||
}
|
map.set(letter, bucket)
|
||||||
map.get(letter)!.push(friend)
|
|
||||||
}
|
}
|
||||||
const letters = [...map.keys()].sort((a, b) => {
|
const letters = [...map.keys()].toSorted((a, b) => {
|
||||||
// '#' 永远排末尾,A-Z 走 localeCompare
|
// '#' 永远排末尾,A-Z 走 localeCompare
|
||||||
if (a === '#') {
|
if (a === '#') {
|
||||||
return 1
|
return 1
|
||||||
|
|
@ -90,7 +89,9 @@ export function useFriendBuckets(
|
||||||
})
|
})
|
||||||
return letters.map((letter) => ({
|
return letters.map((letter) => ({
|
||||||
letter,
|
letter,
|
||||||
list: map.get(letter)!.sort((a, b) => getSortKey(a).localeCompare(getSortKey(b)))
|
list: (map.get(letter) ?? []).toSorted((a, b) =>
|
||||||
|
getSortKey(a).localeCompare(getSortKey(b))
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,7 @@ export const useMessagePuller = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 同一时刻只允许一次 pull:Index.vue 的手动调用与重连 watch 触发可能并发,共用同一个 promise 即可去重 */
|
/** 同一时刻只允许一次 pull:index.vue 的手动调用与重连 watch 触发可能并发,共用同一个 promise 即可去重 */
|
||||||
let pullPromise: null | Promise<void> = null
|
let pullPromise: null | Promise<void> = null
|
||||||
let pullAbortController: AbortController | null = null
|
let pullAbortController: AbortController | null = null
|
||||||
|
|
||||||
|
|
@ -267,7 +267,7 @@ export const useMessagePuller = () => {
|
||||||
*/
|
*/
|
||||||
let pullEpoch = 0
|
let pullEpoch = 0
|
||||||
|
|
||||||
/** 显式取消:仅由 Index.vue onUnmounted(离开 IM / 切账号 / 路由跳出)调用 */
|
/** 显式取消:仅由 index.vue onUnmounted(离开 IM / 切账号 / 路由跳出)调用 */
|
||||||
const cancelPull = () => {
|
const cancelPull = () => {
|
||||||
pullEpoch++
|
pullEpoch++
|
||||||
pullAbortController?.abort()
|
pullAbortController?.abort()
|
||||||
|
|
@ -391,7 +391,7 @@ export const useMessagePuller = () => {
|
||||||
conversationStore.sortConversationList()
|
conversationStore.sortConversationList()
|
||||||
|
|
||||||
// 重连 / 冷启动后补齐当前激活私聊会话的「对方已读位置」
|
// 重连 / 冷启动后补齐当前激活私聊会话的「对方已读位置」
|
||||||
// 离线期间错过的 RECEIPT 推送会被这里补回;其他私聊会话等用户点开时由 Index.vue 的 watch 触发
|
// 离线期间错过的 RECEIPT 推送会被这里补回;其他私聊会话等用户点开时由 index.vue 的 watch 触发
|
||||||
// 私聊已读关闭时跳过,避免打到已禁用接口触发错误日志
|
// 私聊已读关闭时跳过,避免打到已禁用接口触发错误日志
|
||||||
const active = conversationStore.activeConversation
|
const active = conversationStore.activeConversation
|
||||||
if (MESSAGE_PRIVATE_READ_ENABLED && active && active.type === ImConversationType.PRIVATE) {
|
if (MESSAGE_PRIVATE_READ_ENABLED && active && active.type === ImConversationType.PRIVATE) {
|
||||||
|
|
@ -438,7 +438,7 @@ export const useMessagePuller = () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 断网期间 WS 收不到推送:重连后既要按 minId 补齐消息,也要按 update_time + id 补齐好友 / 群 / 群申请状态。
|
* 断网期间 WS 收不到推送:重连后既要按 minId 补齐消息,也要按 update_time + id 补齐好友 / 群 / 群申请状态。
|
||||||
* 首次连接由 Index.vue 显式驱动(pullOnce 拉消息 + 各 store 首拉),这里仅覆盖之后的重连。
|
* 首次连接由 index.vue 显式驱动(pullOnce 拉消息 + 各 store 首拉),这里仅覆盖之后的重连。
|
||||||
* 重连时 store 已就位,pullStateEvents 与 pullOnce 并发即可,无需「先就位再拉消息」的首登顺序约束。
|
* 重连时 store 已就位,pullStateEvents 与 pullOnce 并发即可,无需「先就位再拉消息」的首登顺序约束。
|
||||||
*/
|
*/
|
||||||
watch(
|
watch(
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ export const useMessageSender = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送文本消息(最常用的快捷入口):MessageInput.vue 文本回车走这里
|
* 发送文本消息(最常用的快捷入口):message-input.vue 文本回车走这里
|
||||||
* 返回值:成功 true / 失败 false / 空文本 false(与 sendRaw 对齐,转发场景按返回值判断)
|
* 返回值:成功 true / 失败 false / 空文本 false(与 sendRaw 对齐,转发场景按返回值判断)
|
||||||
*/
|
*/
|
||||||
const send = async (text: string, options?: SendExtOptions): Promise<boolean> => {
|
const send = async (text: string, options?: SendExtOptions): Promise<boolean> => {
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,10 @@ import { preferences } from '@vben/preferences'
|
||||||
|
|
||||||
import { ImConversationType } from '../utils/constants'
|
import { ImConversationType } from '../utils/constants'
|
||||||
import { initDb, stopRequests, StorageKeys } from '../utils/db'
|
import { initDb, stopRequests, StorageKeys } from '../utils/db'
|
||||||
import ContextMenu from './components/ContextMenu.vue'
|
import { ContextMenu, ToolBar } from './components'
|
||||||
import GroupInfoCard from './components/group/GroupInfoCard.vue'
|
import { GroupInfoCard } from './components/group'
|
||||||
import RtcCallContainer from './components/rtc/RtcCallContainer.vue'
|
import { RtcCallContainer } from './components/rtc'
|
||||||
import ToolBar from './components/ToolBar.vue'
|
import { UserInfoCard } from './components/user'
|
||||||
import UserInfoCard from './components/user/UserInfoCard.vue'
|
|
||||||
import { useMessagePuller } from './composables/useMessagePuller'
|
import { useMessagePuller } from './composables/useMessagePuller'
|
||||||
import { useMessageSender } from './composables/useMessageSender'
|
import { useMessageSender } from './composables/useMessageSender'
|
||||||
import { useVoicePlayer } from './composables/useVoicePlayer'
|
import { useVoicePlayer } from './composables/useVoicePlayer'
|
||||||
|
|
@ -209,7 +208,9 @@ watch(
|
||||||
- 右侧 <router-view>:按路由渲染 MessagePage / FriendPage / GroupPage
|
- 右侧 <router-view>:按路由渲染 MessagePage / FriendPage / GroupPage
|
||||||
- 挂载全局弹层:UserInfoCard / GroupInfoCard / ContextMenu
|
- 挂载全局弹层:UserInfoCard / GroupInfoCard / ContextMenu
|
||||||
-->
|
-->
|
||||||
<div class="im-home flex w-full h-full overflow-hidden">
|
<div
|
||||||
|
class="im-home flex w-full h-full overflow-hidden bg-[var(--ant-color-bg-layout)] text-[var(--ant-color-text)]"
|
||||||
|
>
|
||||||
<ToolBar />
|
<ToolBar />
|
||||||
<!--
|
<!--
|
||||||
keep-alive 缓存子页面:
|
keep-alive 缓存子页面:
|
||||||
|
|
@ -237,10 +238,54 @@ watch(
|
||||||
:global(:root) {
|
:global(:root) {
|
||||||
--im-border-color-lighter: #e8eaed;
|
--im-border-color-lighter: #e8eaed;
|
||||||
--im-resize-line-color: #d8dde5;
|
--im-resize-line-color: #d8dde5;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* antd 4.x 不输出 --ant-color-* 全局 CSS 变量(仅 React antd 的 theme.cssVar 支持),
|
||||||
|
* 而 IM 聊天端大量直接使用 var(--ant-color-*)。此处用 Vben design token 兜底定义,
|
||||||
|
* 映射规则与 useAntdDesignTokens 喂给 antd 的 token 完全一致,随主题(亮/暗)自动切换。
|
||||||
|
* 缺失时这些变量为空 → 背景透明、边框/填充失效(典型:表情面板透出输入框占位文字)。
|
||||||
|
*/
|
||||||
|
--ant-color-bg-container: hsl(var(--card));
|
||||||
|
--ant-color-bg-elevated: hsl(var(--popover));
|
||||||
|
--ant-color-bg-layout: hsl(var(--background-deep));
|
||||||
|
--ant-color-border: hsl(var(--border));
|
||||||
|
--ant-color-border-secondary: hsl(var(--border));
|
||||||
|
--ant-color-text: hsl(var(--foreground));
|
||||||
|
--ant-color-text-secondary: hsl(var(--foreground) / 65%);
|
||||||
|
--ant-color-text-placeholder: hsl(var(--foreground) / 45%);
|
||||||
|
--ant-color-text-disabled: hsl(var(--foreground) / 30%);
|
||||||
|
/*
|
||||||
|
* fill 系列:浅色下用 Element Plus 风格的「冷调浅灰实色」而非半透明深色,
|
||||||
|
* 否则面板(会话列表 / 消息面板等)叠在灰底上会显脏发暗,和 Vue3+EP 的干净白差距明显。
|
||||||
|
* 取值依组件实际用法:secondary 多用于面板底(取最浅,对齐 EP --el-bg-color-page #f5f7fa),
|
||||||
|
* tertiary 多用于 hover / 图标块(比面板略深,保证 hover 可见)。深色在 .dark 里另行覆盖。
|
||||||
|
*/
|
||||||
|
--ant-color-fill: #e2e6ec;
|
||||||
|
--ant-color-fill-secondary: #f4f6f9;
|
||||||
|
--ant-color-fill-tertiary: #eaedf2;
|
||||||
|
--ant-color-fill-dark: #d5dae2;
|
||||||
|
--ant-color-primary: hsl(var(--primary));
|
||||||
|
--ant-color-primary-hover: hsl(var(--primary) / 80%);
|
||||||
|
--ant-color-primary-bg: hsl(var(--primary) / 12%);
|
||||||
|
--ant-color-primary-bg-hover: hsl(var(--primary) / 18%);
|
||||||
|
--ant-color-info: hsl(var(--primary));
|
||||||
|
--ant-color-success: hsl(var(--success));
|
||||||
|
--ant-color-success-bg: hsl(var(--success) / 12%);
|
||||||
|
--ant-color-success-bg-hover: hsl(var(--success) / 18%);
|
||||||
|
--ant-color-warning: hsl(var(--warning));
|
||||||
|
--ant-color-warning-bg: hsl(var(--warning) / 12%);
|
||||||
|
--ant-color-warning-bg-hover: hsl(var(--warning) / 18%);
|
||||||
|
--ant-color-error: hsl(var(--destructive));
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) {
|
:global(.dark) {
|
||||||
--im-border-color-lighter: rgb(255 255 255 / 12%);
|
--im-border-color-lighter: rgb(255 255 255 / 12%);
|
||||||
--im-resize-line-color: rgb(255 255 255 / 18%);
|
--im-resize-line-color: rgb(255 255 255 / 18%);
|
||||||
|
|
||||||
|
/* 深色下 fill 回到「基于前景色的半透明亮色」,叠在深底上得到自然的微亮表面 */
|
||||||
|
--ant-color-fill: hsl(var(--foreground) / 12%);
|
||||||
|
--ant-color-fill-secondary: hsl(var(--foreground) / 8%);
|
||||||
|
--ant-color-fill-tertiary: hsl(var(--foreground) / 4%);
|
||||||
|
--ant-color-fill-dark: hsl(var(--foreground) / 18%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { ref, toRef } from 'vue'
|
||||||
|
|
||||||
import { IconifyIcon as Icon } from '@vben/icons'
|
import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
|
|
||||||
import FriendItem from '../../components/friend/FriendItem.vue'
|
import { FriendItem } from '../../components/friend'
|
||||||
import { useFriendBuckets } from '../../composables/useFriendBuckets'
|
import { useFriendBuckets } from '../../composables/useFriendBuckets'
|
||||||
|
|
||||||
defineOptions({ name: 'ImContactFriendList' })
|
defineOptions({ name: 'ImContactFriendList' })
|
||||||
|
|
@ -12,8 +12,8 @@ import { Button, Input, message } from 'ant-design-vue'
|
||||||
import { getCurrentUserId } from '#/views/im/utils/auth'
|
import { getCurrentUserId } from '#/views/im/utils/auth'
|
||||||
|
|
||||||
import { ImFriendRequestHandleResult } from '../../../utils/constants'
|
import { ImFriendRequestHandleResult } from '../../../utils/constants'
|
||||||
import UserAvatar from '../../components/user/UserAvatar.vue'
|
import { UserAvatar } from '../../components/user'
|
||||||
import UserInfo from '../../components/user/UserInfo.vue'
|
import { UserInfo } from '../../components/user'
|
||||||
import { useFriendStore } from '../../store/friendStore'
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
|
|
||||||
defineOptions({ name: 'ImContactFriendRequestDetail' })
|
defineOptions({ name: 'ImContactFriendRequestDetail' })
|
||||||
|
|
@ -11,7 +11,7 @@ import { Badge } from 'ant-design-vue'
|
||||||
|
|
||||||
import { getCurrentUserId } from '#/views/im/utils/auth'
|
import { getCurrentUserId } from '#/views/im/utils/auth'
|
||||||
|
|
||||||
import UserAvatar from '../../components/user/UserAvatar.vue'
|
import { UserAvatar } from '../../components/user'
|
||||||
import { useFriendStore } from '../../store/friendStore'
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
|
|
||||||
defineOptions({ name: 'ImContactFriendRequestList' })
|
defineOptions({ name: 'ImContactFriendRequestList' })
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupLite } from '../../types'
|
import type { GroupLite } from '../../types'
|
||||||
|
|
||||||
import GroupInfo from '../../components/group/GroupInfo.vue'
|
import { GroupInfo } from '../../components/group'
|
||||||
|
|
||||||
defineOptions({ name: 'ImContactGroupDetail' })
|
defineOptions({ name: 'ImContactGroupDetail' })
|
||||||
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import { IconifyIcon as Icon } from '@vben/icons'
|
import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
|
|
||||||
import GroupItem from '../../components/group/GroupItem.vue'
|
import { GroupItem } from '../../components/group'
|
||||||
|
|
||||||
defineOptions({ name: 'ImContactGroupList' })
|
defineOptions({ name: 'ImContactGroupList' })
|
||||||
|
|
||||||
|
|
@ -12,16 +12,16 @@ import { Input, message } from 'ant-design-vue'
|
||||||
import { ImConversationType } from '../../../utils/constants'
|
import { ImConversationType } from '../../../utils/constants'
|
||||||
import { StorageKeys } from '../../../utils/db'
|
import { StorageKeys } from '../../../utils/db'
|
||||||
import { getFriendDisplayName, getGroupDisplayName, isGroupQuit } from '../../../utils/user'
|
import { getFriendDisplayName, getGroupDisplayName, isGroupQuit } from '../../../utils/user'
|
||||||
import ResizableAside from '../../components/ResizableAside.vue'
|
import { ResizableAside } from '../../components'
|
||||||
import UserInfo from '../../components/user/UserInfo.vue'
|
import { UserInfo } from '../../components/user'
|
||||||
import { useConversationStore } from '../../store/conversationStore'
|
import { useConversationStore } from '../../store/conversationStore'
|
||||||
import { useFriendStore } from '../../store/friendStore'
|
import { useFriendStore } from '../../store/friendStore'
|
||||||
import { useGroupStore } from '../../store/groupStore'
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
import FriendList from './FriendList.vue'
|
import FriendList from './friend-list.vue'
|
||||||
import FriendRequestDetail from './FriendRequestDetail.vue'
|
import FriendRequestDetail from './friend-request-detail.vue'
|
||||||
import FriendRequestList from './FriendRequestList.vue'
|
import FriendRequestList from './friend-request-list.vue'
|
||||||
import GroupDetail from './GroupDetail.vue'
|
import GroupDetail from './group-detail.vue'
|
||||||
import GroupList from './GroupList.vue'
|
import GroupList from './group-list.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImContactPage' })
|
defineOptions({ name: 'ImContactPage' })
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
import type { GroupMemberLite } from '../../../../components/group'
|
||||||
import type { Conversation, GroupLite } from '../../../../types'
|
import type { Conversation, GroupLite } from '../../../../types'
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
@ -21,13 +21,13 @@ import { ImConversationType, ImGroupMemberRole } from '#/views/im/utils/constant
|
||||||
import { toGroupCardTarget } from '#/views/im/utils/message'
|
import { toGroupCardTarget } from '#/views/im/utils/message'
|
||||||
import { isGroupQuit } from '#/views/im/utils/user'
|
import { isGroupQuit } from '#/views/im/utils/user'
|
||||||
|
|
||||||
import GroupAdminSetDialog from '../../../../components/group/GroupAdminSetDialog.vue'
|
import { GroupAdminSetDialog } from '../../../../components/group'
|
||||||
import GroupMemberAddDialog from '../../../../components/group/GroupMemberAddDialog.vue'
|
import { GroupMemberAddDialog } from '../../../../components/group'
|
||||||
import GroupMemberGrid from '../../../../components/group/GroupMemberGrid.vue'
|
import { GroupMemberGrid } from '../../../../components/group'
|
||||||
import GroupMemberRemoveDialog from '../../../../components/group/GroupMemberRemoveDialog.vue'
|
import { GroupMemberRemoveDialog } from '../../../../components/group'
|
||||||
import GroupOwnerTransferDialog from '../../../../components/group/GroupOwnerTransferDialog.vue'
|
import { GroupOwnerTransferDialog } from '../../../../components/group'
|
||||||
import GroupRequestListDialog from '../../../../components/group/GroupRequestListDialog.vue'
|
import { GroupRequestListDialog } from '../../../../components/group'
|
||||||
import RecommendCardDialog from '../../../../components/user/RecommendCardDialog.vue'
|
import { RecommendCardDialog } from '../../../../components/user'
|
||||||
import { useConversationStore } from '../../../../store/conversationStore'
|
import { useConversationStore } from '../../../../store/conversationStore'
|
||||||
import { useGroupStore } from '../../../../store/groupStore'
|
import { useGroupStore } from '../../../../store/groupStore'
|
||||||
|
|
||||||
|
|
@ -827,7 +827,8 @@ function handleOpenTransferOwner() {
|
||||||
border-top: 1px solid var(--im-border-color-lighter);
|
border-top: 1px solid var(--im-border-color-lighter);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .im-conversation-group-side__modal {
|
/* 整体放进 :global(),避免 Vue scoped 把 `:global(.dark) .xxx` 塌缩成裸 `.dark` 而把变量刷到 <html> */
|
||||||
|
:global(.dark .im-conversation-group-side__modal) {
|
||||||
--im-conversation-side-bg: rgb(255 255 255 / 5%);
|
--im-conversation-side-bg: rgb(255 255 255 / 5%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -13,8 +13,8 @@ import { formatConversationTime } from '#/views/im/utils/time'
|
||||||
import { getSenderDisplayName } from '#/views/im/utils/user'
|
import { getSenderDisplayName } from '#/views/im/utils/user'
|
||||||
|
|
||||||
import { ImContentType, ImConversationType, isNormalMessage } from '../../../../../utils/constants'
|
import { ImContentType, ImConversationType, isNormalMessage } from '../../../../../utils/constants'
|
||||||
import GroupAvatar from '../../../../components/group/GroupAvatar.vue'
|
import { GroupAvatar } from '../../../../components/group'
|
||||||
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
import { UserAvatar } from '../../../../components/user'
|
||||||
import { useConversationStore } from '../../../../store/conversationStore'
|
import { useConversationStore } from '../../../../store/conversationStore'
|
||||||
import { useFriendStore } from '../../../../store/friendStore'
|
import { useFriendStore } from '../../../../store/friendStore'
|
||||||
import { useGroupRequestStore } from '../../../../store/groupRequestStore'
|
import { useGroupRequestStore } from '../../../../store/groupRequestStore'
|
||||||
|
|
@ -13,8 +13,8 @@ import { useGroupStore } from '#/views/im/home/store/groupStore'
|
||||||
import { ImConversationType } from '#/views/im/utils/constants'
|
import { ImConversationType } from '#/views/im/utils/constants'
|
||||||
import { getFriendDisplayName } from '#/views/im/utils/user'
|
import { getFriendDisplayName } from '#/views/im/utils/user'
|
||||||
|
|
||||||
import GroupCreateDialog from '../../../../components/group/GroupCreateDialog.vue'
|
import { GroupCreateDialog } from '../../../../components/group'
|
||||||
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
import { UserAvatar } from '../../../../components/user'
|
||||||
|
|
||||||
defineOptions({ name: 'ImConversationPrivateSide' })
|
defineOptions({ name: 'ImConversationPrivateSide' })
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as ConversationGroupSide } from './conversation-group-side.vue';
|
||||||
|
export { default as ConversationItem } from './conversation-item.vue';
|
||||||
|
export { default as ConversationPrivateSide } from './conversation-private-side.vue';
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './conversation';
|
||||||
|
export * from './input';
|
||||||
|
export * from './message';
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export { default as FacePicker } from './face-picker.vue';
|
||||||
|
export { default as MentionPicker } from './mention-picker.vue';
|
||||||
|
export { default as MessageInput } from './message-input.vue';
|
||||||
|
export { default as MessageMultiSelectBar } from './message-multi-select-bar.vue';
|
||||||
|
export { default as VoiceRecorder } from './voice-recorder.vue';
|
||||||
|
|
@ -7,7 +7,7 @@ import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
import { getCurrentUserId } from '#/views/im/utils/auth'
|
import { getCurrentUserId } from '#/views/im/utils/auth'
|
||||||
import { IM_AT_ALL_NICKNAME, IM_AT_ALL_USER_ID } from '#/views/im/utils/constants'
|
import { IM_AT_ALL_NICKNAME, IM_AT_ALL_USER_ID } from '#/views/im/utils/constants'
|
||||||
|
|
||||||
import GroupMember, { type GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
import { GroupMember, type GroupMemberLite } from '../../../../components/group'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMentionPicker' })
|
defineOptions({ name: 'ImMentionPicker' })
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
import type { GroupMemberLite } from '../../../../components/group'
|
||||||
|
|
||||||
import type { Conversation } from '#/views/im/home/types'
|
import type { Conversation } from '#/views/im/home/types'
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from
|
||||||
import { IconifyIcon as Icon } from '@vben/icons'
|
import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
import { isOpenableUrl } from '@vben/utils'
|
import { isOpenableUrl } from '@vben/utils'
|
||||||
|
|
||||||
import { Button, Dropdown, Menu, message, Tooltip } from 'ant-design-vue'
|
import { Button, DropdownButton, Menu, message, Tooltip } from 'ant-design-vue'
|
||||||
|
|
||||||
import { uploadFile } from '#/api/infra/file'
|
import { uploadFile } from '#/api/infra/file'
|
||||||
import {
|
import {
|
||||||
|
|
@ -32,10 +32,10 @@ import {
|
||||||
} from '#/views/im/utils/message'
|
} from '#/views/im/utils/message'
|
||||||
import { getMemberDisplayName } from '#/views/im/utils/user'
|
import { getMemberDisplayName } from '#/views/im/utils/user'
|
||||||
|
|
||||||
import ReplyPreview from '../message/ReplyPreview.vue'
|
import { ReplyPreview } from '../message'
|
||||||
import FacePicker from './FacePicker.vue'
|
import FacePicker from './face-picker.vue'
|
||||||
import MentionPicker from './MentionPicker.vue'
|
import MentionPicker from './mention-picker.vue'
|
||||||
import VoiceRecorder from './VoiceRecorder.vue'
|
import VoiceRecorder from './voice-recorder.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessageInput' })
|
defineOptions({ name: 'ImMessageInput' })
|
||||||
|
|
||||||
|
|
@ -1110,9 +1110,9 @@ async function onVideoPicked(e: Event) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 群聊 + 群已读开启:发送按钮 + ▼ 下拉菜单(点主按钮普通发送 / 点 ▼ 选「发送回执消息」),对齐微信 PC -->
|
<!-- 群聊 + 群已读开启:发送按钮 + ▼ 下拉菜单(点主按钮普通发送 / 点 ▼ 选「发送回执消息」),对齐微信 PC -->
|
||||||
<Dropdown
|
<!-- 注意:ant-design-vue 的分裂按钮是 DropdownButton(非 Dropdown 的 split-button 属性,那是 Element Plus 写法) -->
|
||||||
|
<DropdownButton
|
||||||
v-if="isGroup && MESSAGE_GROUP_READ_ENABLED"
|
v-if="isGroup && MESSAGE_GROUP_READ_ENABLED"
|
||||||
split-button
|
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="!canSend"
|
:disabled="!canSend"
|
||||||
@click="handleSend()"
|
@click="handleSend()"
|
||||||
|
|
@ -1123,7 +1123,7 @@ async function onVideoPicked(e: Event) {
|
||||||
<Menu.Item key="receipt">发送回执消息</Menu.Item>
|
<Menu.Item key="receipt">发送回执消息</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</DropdownButton>
|
||||||
<!-- 私聊或群已读关闭:普通发送按钮(无群回执入口) -->
|
<!-- 私聊或群已读关闭:普通发送按钮(无群回执入口) -->
|
||||||
<Button v-else type="primary" :disabled="!canSend" @click="handleSend()">
|
<Button v-else type="primary" :disabled="!canSend" @click="handleSend()">
|
||||||
发 送
|
发 送
|
||||||
|
|
@ -1186,13 +1186,16 @@ async function onVideoPicked(e: Event) {
|
||||||
border-color: #ffa39e;
|
border-color: #ffa39e;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .message-input__mute-overlay--warning {
|
/* 注意:必须把 .dark 与目标类整体放进 :global(),否则 Vue scoped 编译会把
|
||||||
|
`:global(.dark) .xxx` 错误塌缩成裸 `.dark`,导致这些颜色被直接刷到 <html> 上
|
||||||
|
(暗色下整页发红/发暗黄)。BEM 类名唯一,整体 global 不会误伤其它元素。 */
|
||||||
|
:global(.dark .message-input__mute-overlay--warning) {
|
||||||
color: #ffd666;
|
color: #ffd666;
|
||||||
background: rgb(77 56 21 / 88%);
|
background: rgb(77 56 21 / 88%);
|
||||||
border-color: #ad6800;
|
border-color: #ad6800;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .message-input__mute-overlay--error {
|
:global(.dark .message-input__mute-overlay--error) {
|
||||||
color: #ff7875;
|
color: #ff7875;
|
||||||
background: rgb(91 33 33 / 88%);
|
background: rgb(91 33 33 / 88%);
|
||||||
border-color: #a8071a;
|
border-color: #a8071a;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as MessageForwardDialog } from './message-forward-dialog.vue';
|
||||||
|
export { default as MessageMergeDetailDialog } from './message-merge-detail-dialog.vue';
|
||||||
|
|
@ -8,8 +8,8 @@ import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
import { Button, Input, message, Modal } from 'ant-design-vue'
|
import { Button, Input, message, Modal } from 'ant-design-vue'
|
||||||
|
|
||||||
import { createGroup } from '#/api/im/group'
|
import { createGroup } from '#/api/im/group'
|
||||||
import ConversationPickerPanel from '#/views/im/home/components/picker/ConversationPickerPanel.vue'
|
import { ConversationPickerPanel } from '#/views/im/home/components/picker'
|
||||||
import FriendPickerPanel from '#/views/im/home/components/picker/FriendPickerPanel.vue'
|
import { FriendPickerPanel } from '#/views/im/home/components/picker'
|
||||||
import { useMessageMultiSelect } from '#/views/im/home/composables/useMessageMultiSelect'
|
import { useMessageMultiSelect } from '#/views/im/home/composables/useMessageMultiSelect'
|
||||||
import { useMessageSender } from '#/views/im/home/composables/useMessageSender'
|
import { useMessageSender } from '#/views/im/home/composables/useMessageSender'
|
||||||
import { useConversationStore } from '#/views/im/home/store/conversationStore'
|
import { useConversationStore } from '#/views/im/home/store/conversationStore'
|
||||||
|
|
@ -31,7 +31,7 @@ import {
|
||||||
} from '#/views/im/utils/message'
|
} from '#/views/im/utils/message'
|
||||||
import { isGroupQuit } from '#/views/im/utils/user'
|
import { isGroupQuit } from '#/views/im/utils/user'
|
||||||
|
|
||||||
import FacePicker from '../../input/FacePicker.vue'
|
import { FacePicker } from '../../input'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessageForwardDialog' })
|
defineOptions({ name: 'ImMessageForwardDialog' })
|
||||||
|
|
||||||
|
|
@ -304,6 +304,7 @@ async function handleCreateGroupAndSend() {
|
||||||
v-model:open="visible"
|
v-model:open="visible"
|
||||||
width="720px"
|
width="720px"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
|
:footer="view === 'conversation' ? null : undefined"
|
||||||
class="im-picker-dialog im-forward-dialog"
|
class="im-picker-dialog im-forward-dialog"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
|
|
@ -5,12 +5,12 @@ import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
|
|
||||||
import { Modal } from 'ant-design-vue'
|
import { Modal } from 'ant-design-vue'
|
||||||
|
|
||||||
import UserAvatar from '#/views/im/home/components/user/UserAvatar.vue'
|
import { UserAvatar } from '#/views/im/home/components/user'
|
||||||
import { useVoicePlayer } from '#/views/im/home/composables/useVoicePlayer'
|
import { useVoicePlayer } from '#/views/im/home/composables/useVoicePlayer'
|
||||||
import { type MergeMessage, parseMessage } from '#/views/im/utils/message'
|
import { type MergeMessage, parseMessage } from '#/views/im/utils/message'
|
||||||
import { formatMergeItemTime } from '#/views/im/utils/time'
|
import { formatMergeItemTime } from '#/views/im/utils/time'
|
||||||
|
|
||||||
import MessageBubble from '../MessageBubble.vue'
|
import { MessageBubble } from '..'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessageMergeDetailDialog' })
|
defineOptions({ name: 'ImMessageMergeDetailDialog' })
|
||||||
|
|
||||||
|
|
@ -60,6 +60,7 @@ function handleClose() {
|
||||||
<Modal
|
<Modal
|
||||||
v-model:open="visible"
|
v-model:open="visible"
|
||||||
width="560px"
|
width="560px"
|
||||||
|
:footer="null"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
class="im-merge-detail-dialog"
|
class="im-merge-detail-dialog"
|
||||||
:closable="true"
|
:closable="true"
|
||||||
|
|
@ -6,7 +6,7 @@ import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
import { getCurrentUserId } from '#/views/im/utils/auth'
|
import { getCurrentUserId } from '#/views/im/utils/auth'
|
||||||
import { ImGroupMemberRole } from '#/views/im/utils/constants'
|
import { ImGroupMemberRole } from '#/views/im/utils/constants'
|
||||||
|
|
||||||
import GroupRequestListDialog from '../../../../components/group/GroupRequestListDialog.vue'
|
import { GroupRequestListDialog } from '../../../../components/group'
|
||||||
import { useGroupRequestStore } from '../../../../store/groupRequestStore'
|
import { useGroupRequestStore } from '../../../../store/groupRequestStore'
|
||||||
import { useGroupStore } from '../../../../store/groupStore'
|
import { useGroupStore } from '../../../../store/groupStore'
|
||||||
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
export { default as GroupPinnedMessage } from './group-pinned-message.vue';
|
||||||
|
export { default as GroupRequestPending } from './group-request-pending.vue';
|
||||||
|
export { default as MaterialBubble } from './material-bubble.vue';
|
||||||
|
export { default as MessageBubble } from './message-bubble.vue';
|
||||||
|
export { default as MessageHistory } from './message-history.vue';
|
||||||
|
export { default as MessageItem } from './message-item.vue';
|
||||||
|
export { default as MessagePanel } from './message-panel.vue';
|
||||||
|
export { default as MessageReadStatus } from './message-read-status.vue';
|
||||||
|
export { default as ReplyPreview } from './reply-preview.vue';
|
||||||
|
export { default as TipSegments } from './tip-segments.vue';
|
||||||
|
|
@ -6,7 +6,7 @@ import { formatFileSize, openSafeUrl } from '@vben/utils'
|
||||||
|
|
||||||
import { Image } from 'ant-design-vue'
|
import { Image } from 'ant-design-vue'
|
||||||
|
|
||||||
import CardBubble from '#/views/im/home/components/card/CardBubble.vue'
|
import { CardBubble } from '#/views/im/home/components/card'
|
||||||
import { useVoicePlayer } from '#/views/im/home/composables/useVoicePlayer'
|
import { useVoicePlayer } from '#/views/im/home/composables/useVoicePlayer'
|
||||||
import { MESSAGE_MERGE_PREVIEW_LINES } from '#/views/im/utils/config'
|
import { MESSAGE_MERGE_PREVIEW_LINES } from '#/views/im/utils/config'
|
||||||
import { ImContentType } from '#/views/im/utils/constants'
|
import { ImContentType } from '#/views/im/utils/constants'
|
||||||
|
|
@ -27,8 +27,8 @@ import {
|
||||||
} from '#/views/im/utils/message'
|
} from '#/views/im/utils/message'
|
||||||
import { formatSeconds } from '#/views/im/utils/time'
|
import { formatSeconds } from '#/views/im/utils/time'
|
||||||
|
|
||||||
import MaterialBubble from './MaterialBubble.vue'
|
import MaterialBubble from './material-bubble.vue'
|
||||||
import TipSegments from './TipSegments.vue'
|
import TipSegments from './tip-segments.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessageBubble' })
|
defineOptions({ name: 'ImMessageBubble' })
|
||||||
|
|
||||||
|
|
@ -384,7 +384,8 @@ onBeforeUnmount(() => {
|
||||||
border-color: transparent transparent transparent #95ec69;
|
border-color: transparent transparent transparent #95ec69;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .message-bubble--other {
|
/* 整体放进 :global(),避免 Vue scoped 把 `:global(.dark) .xxx` 塌缩成裸 `.dark` 而把变量刷到 <html> */
|
||||||
|
:global(.dark .message-bubble--other) {
|
||||||
--im-message-bubble-other-bg: rgb(255 255 255 / 12%);
|
--im-message-bubble-other-bg: rgb(255 255 255 / 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,15 +47,15 @@ import {
|
||||||
getSenderRealNickname
|
getSenderRealNickname
|
||||||
} from '#/views/im/utils/user'
|
} from '#/views/im/utils/user'
|
||||||
|
|
||||||
import GroupMember, { type GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
import { GroupMember, type GroupMemberLite } from '../../../../components/group'
|
||||||
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
import { UserAvatar } from '../../../../components/user'
|
||||||
import { useConversationStore } from '../../../../store/conversationStore'
|
import { useConversationStore } from '../../../../store/conversationStore'
|
||||||
import { useFriendStore } from '../../../../store/friendStore'
|
import { useFriendStore } from '../../../../store/friendStore'
|
||||||
import { useGroupStore } from '../../../../store/groupStore'
|
import { useGroupStore } from '../../../../store/groupStore'
|
||||||
import { useMessageStore } from '../../../../store/messageStore'
|
import { useMessageStore } from '../../../../store/messageStore'
|
||||||
import { IM_MERGE_DETAIL_DIALOG_KEY } from './forward/keys'
|
import { IM_MERGE_DETAIL_DIALOG_KEY } from './forward/keys'
|
||||||
import MessageBubble from './MessageBubble.vue'
|
import MessageBubble from './message-bubble.vue'
|
||||||
import TipSegments from './TipSegments.vue'
|
import TipSegments from './tip-segments.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessageHistory' })
|
defineOptions({ name: 'ImMessageHistory' })
|
||||||
|
|
||||||
|
|
@ -508,6 +508,7 @@ function locateMessage(messageId: number) {
|
||||||
v-model:open="visible"
|
v-model:open="visible"
|
||||||
:title="title"
|
:title="title"
|
||||||
width="640px"
|
width="640px"
|
||||||
|
:footer="null"
|
||||||
class="im-message-history__dialog"
|
class="im-message-history__dialog"
|
||||||
@after-open-change="(open) => open && onDialogOpen()"
|
@after-open-change="(open) => open && onDialogOpen()"
|
||||||
>
|
>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
import type { GroupMemberLite } from '../../../../components/group'
|
||||||
import type { Message } from '../../../../types'
|
import type { Message } from '../../../../types'
|
||||||
|
|
||||||
import { computed, inject } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
|
|
@ -60,7 +60,7 @@ import {
|
||||||
isGroupQuit
|
isGroupQuit
|
||||||
} from '#/views/im/utils/user'
|
} from '#/views/im/utils/user'
|
||||||
|
|
||||||
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
import { UserAvatar } from '../../../../components/user'
|
||||||
import { mediaTypeHandlers, useMediaUploader } from '../../../../composables/useMediaUploader'
|
import { mediaTypeHandlers, useMediaUploader } from '../../../../composables/useMediaUploader'
|
||||||
import { useMessageMultiSelect } from '../../../../composables/useMessageMultiSelect'
|
import { useMessageMultiSelect } from '../../../../composables/useMessageMultiSelect'
|
||||||
import { useMessageSender } from '../../../../composables/useMessageSender'
|
import { useMessageSender } from '../../../../composables/useMessageSender'
|
||||||
|
|
@ -76,10 +76,10 @@ import {
|
||||||
IM_MERGE_DETAIL_DIALOG_KEY,
|
IM_MERGE_DETAIL_DIALOG_KEY,
|
||||||
IM_RTC_REDIAL_KEY
|
IM_RTC_REDIAL_KEY
|
||||||
} from './forward/keys'
|
} from './forward/keys'
|
||||||
import MessageBubble from './MessageBubble.vue'
|
import MessageBubble from './message-bubble.vue'
|
||||||
import MessageReadStatus from './MessageReadStatus.vue'
|
import MessageReadStatus from './message-read-status.vue'
|
||||||
import ReplyPreview from './ReplyPreview.vue'
|
import ReplyPreview from './reply-preview.vue'
|
||||||
import TipSegments from './TipSegments.vue'
|
import TipSegments from './tip-segments.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessageItem' })
|
defineOptions({ name: 'ImMessageItem' })
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
import type { GroupMemberLite } from '../../../../components/group'
|
||||||
import type { GroupLite } from '../../../../types'
|
import type { GroupLite } from '../../../../types'
|
||||||
|
|
||||||
import { computed, nextTick, provide, ref, watch } from 'vue'
|
import { computed, nextTick, provide, ref, watch } from 'vue'
|
||||||
|
|
@ -14,9 +14,11 @@ import { getClientConversationId } from '#/views/im/utils/db'
|
||||||
import { resolveCallEndReasonText } from '#/views/im/utils/message'
|
import { resolveCallEndReasonText } from '#/views/im/utils/message'
|
||||||
import { getMemberDisplayName, isGroupQuit } from '#/views/im/utils/user'
|
import { getMemberDisplayName, isGroupQuit } from '#/views/im/utils/user'
|
||||||
|
|
||||||
import GroupMuteMemberDialog from '../../../../components/group/GroupMuteMemberDialog.vue'
|
import { GroupMuteMemberDialog } from '../../../../components/group'
|
||||||
import RtcCallMemberPickerDialog from '../../../../components/rtc/RtcCallMemberPickerDialog.vue'
|
import {
|
||||||
import RtcGroupCallBanner from '../../../../components/rtc/RtcGroupCallBanner.vue'
|
RtcCallMemberPickerDialog,
|
||||||
|
RtcGroupCallBanner
|
||||||
|
} from '../../../../components/rtc'
|
||||||
import { useMessageMultiSelect } from '../../../../composables/useMessageMultiSelect'
|
import { useMessageMultiSelect } from '../../../../composables/useMessageMultiSelect'
|
||||||
import { useVoicePlayer } from '../../../../composables/useVoicePlayer'
|
import { useVoicePlayer } from '../../../../composables/useVoicePlayer'
|
||||||
import { useConversationStore } from '../../../../store/conversationStore'
|
import { useConversationStore } from '../../../../store/conversationStore'
|
||||||
|
|
@ -25,21 +27,21 @@ import { useGroupStore } from '../../../../store/groupStore'
|
||||||
import { useMessageStore } from '../../../../store/messageStore'
|
import { useMessageStore } from '../../../../store/messageStore'
|
||||||
import { useRtcStore } from '../../../../store/rtcStore'
|
import { useRtcStore } from '../../../../store/rtcStore'
|
||||||
import { useImUiStore } from '../../../../store/uiStore'
|
import { useImUiStore } from '../../../../store/uiStore'
|
||||||
import ConversationGroupSide from '../conversation/ConversationGroupSide.vue'
|
import { ConversationGroupSide } from '../conversation'
|
||||||
import ConversationPrivateSide from '../conversation/ConversationPrivateSide.vue'
|
import { ConversationPrivateSide } from '../conversation'
|
||||||
import MessageInput from '../input/MessageInput.vue'
|
import { MessageInput } from '../input'
|
||||||
import MessageMultiSelectBar from '../input/MessageMultiSelectBar.vue'
|
import { MessageMultiSelectBar } from '../input'
|
||||||
|
import { MessageForwardDialog } from './forward'
|
||||||
|
import { MessageMergeDetailDialog } from './forward'
|
||||||
import {
|
import {
|
||||||
IM_FORWARD_DIALOG_KEY,
|
IM_FORWARD_DIALOG_KEY,
|
||||||
IM_MERGE_DETAIL_DIALOG_KEY,
|
IM_MERGE_DETAIL_DIALOG_KEY,
|
||||||
IM_RTC_REDIAL_KEY
|
IM_RTC_REDIAL_KEY
|
||||||
} from './forward/keys'
|
} from './forward/keys'
|
||||||
import MessageForwardDialog from './forward/MessageForwardDialog.vue'
|
import GroupPinnedMessage from './group-pinned-message.vue'
|
||||||
import MessageMergeDetailDialog from './forward/MessageMergeDetailDialog.vue'
|
import GroupRequestPending from './group-request-pending.vue'
|
||||||
import GroupPinnedMessage from './GroupPinnedMessage.vue'
|
import MessageHistory from './message-history.vue'
|
||||||
import GroupRequestPending from './GroupRequestPending.vue'
|
import MessageItem from './message-item.vue'
|
||||||
import MessageHistory from './MessageHistory.vue'
|
|
||||||
import MessageItem from './MessageItem.vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessagePanel' })
|
defineOptions({ name: 'ImMessagePanel' })
|
||||||
|
|
||||||
|
|
@ -10,8 +10,8 @@ import { Popover, TabPane, Tabs } from 'ant-design-vue'
|
||||||
import { getGroupReadUsers as apiGetGroupReadUsers } from '#/api/im/message/group'
|
import { getGroupReadUsers as apiGetGroupReadUsers } from '#/api/im/message/group'
|
||||||
|
|
||||||
import { ImConversationType, ImMessageReceiptStatus } from '../../../../../utils/constants'
|
import { ImConversationType, ImMessageReceiptStatus } from '../../../../../utils/constants'
|
||||||
import GroupMember, { type GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
import { PagedScroller } from '../../../../components'
|
||||||
import PagedScroller from '../../../../components/PagedScroller.vue'
|
import { GroupMember, type GroupMemberLite } from '../../../../components/group'
|
||||||
import { useMessageStore } from '../../../../store/messageStore'
|
import { useMessageStore } from '../../../../store/messageStore'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessageReadStatus' })
|
defineOptions({ name: 'ImMessageReadStatus' })
|
||||||
|
|
@ -4,7 +4,7 @@ import { computed } from 'vue'
|
||||||
import { IconifyIcon as Icon } from '@vben/icons'
|
import { IconifyIcon as Icon } from '@vben/icons'
|
||||||
import { formatFileSize } from '@vben/utils'
|
import { formatFileSize } from '@vben/utils'
|
||||||
|
|
||||||
import CardLineLabel from '#/views/im/home/components/card/CardLineLabel.vue'
|
import { CardLineLabel } from '#/views/im/home/components/card'
|
||||||
import { ImContentType } from '#/views/im/utils/constants'
|
import { ImContentType } from '#/views/im/utils/constants'
|
||||||
import { getClientConversationId } from '#/views/im/utils/db'
|
import { getClientConversationId } from '#/views/im/utils/db'
|
||||||
import {
|
import {
|
||||||
|
|
@ -10,14 +10,13 @@ import { Button, Dropdown, Input, Menu } from 'ant-design-vue'
|
||||||
import { ImConversationType } from '../../../utils/constants'
|
import { ImConversationType } from '../../../utils/constants'
|
||||||
import { filterConversationsByKeyword, getConversationKey } from '../../../utils/conversation'
|
import { filterConversationsByKeyword, getConversationKey } from '../../../utils/conversation'
|
||||||
import { StorageKeys } from '../../../utils/db'
|
import { StorageKeys } from '../../../utils/db'
|
||||||
import FriendAddDialog from '../../components/friend/FriendAddDialog.vue'
|
import { ResizableAside } from '../../components'
|
||||||
import GroupCreateDialog from '../../components/group/GroupCreateDialog.vue'
|
import { FriendAddDialog } from '../../components/friend'
|
||||||
import ResizableAside from '../../components/ResizableAside.vue'
|
import { GroupCreateDialog } from '../../components/group'
|
||||||
import { useConversationStore } from '../../store/conversationStore'
|
import { useConversationStore } from '../../store/conversationStore'
|
||||||
import { useGroupStore } from '../../store/groupStore'
|
import { useGroupStore } from '../../store/groupStore'
|
||||||
import { useImUiStore } from '../../store/uiStore'
|
import { useImUiStore } from '../../store/uiStore'
|
||||||
import ConversationItem from './components/conversation/ConversationItem.vue'
|
import { ConversationItem, MessagePanel } from './components'
|
||||||
import MessagePanel from './components/message/MessagePanel.vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessagePage' })
|
defineOptions({ name: 'ImMessagePage' })
|
||||||
|
|
||||||
|
|
@ -200,12 +199,16 @@ watch(() => uiStore.nextUnreadJumpNonce, jumpToNextUnread)
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Item @click="handleOpenCreateGroup">
|
<Menu.Item @click="handleOpenCreateGroup">
|
||||||
<Icon icon="ant-design:message-outlined" :size="16" />
|
<span class="inline-flex items-center gap-2">
|
||||||
<span>发起群聊</span>
|
<Icon icon="ant-design:message-outlined" :size="16" />
|
||||||
|
<span>发起群聊</span>
|
||||||
|
</span>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item @click="friendAddDialogRef?.open()">
|
<Menu.Item @click="friendAddDialogRef?.open()">
|
||||||
<Icon icon="ant-design:user-add-outlined" :size="16" />
|
<span class="inline-flex items-center gap-2">
|
||||||
<span>添加朋友</span>
|
<Icon icon="ant-design:user-add-outlined" :size="16" />
|
||||||
|
<span>添加朋友</span>
|
||||||
|
</span>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { ImFriendAddSource } from '../../utils/constants'
|
||||||
*
|
*
|
||||||
* 收纳标准:触发点 N 个、挂载点想保持 1 个的浮层状态。
|
* 收纳标准:触发点 N 个、挂载点想保持 1 个的浮层状态。
|
||||||
* 任意位置都可能 open,但 DOM 上只想留一份实例 → 走 store 派发,
|
* 任意位置都可能 open,但 DOM 上只想留一份实例 → 走 store 派发,
|
||||||
* 由 `Index.vue` 挂一个订阅组件统一渲染。
|
* 由 `index.vue` 挂一个订阅组件统一渲染。
|
||||||
*/
|
*/
|
||||||
export const useImUiStore = defineStore('imUiStore', () => {
|
export const useImUiStore = defineStore('imUiStore', () => {
|
||||||
// ==================== 用户名片 UserInfoCard ====================
|
// ==================== 用户名片 UserInfoCard ====================
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as ChannelSelect } from './select.vue';
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as MaterialSelect } from './select.vue';
|
||||||
|
|
@ -8,7 +8,7 @@ import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { z } from '#/adapter/form';
|
import { z } from '#/adapter/form';
|
||||||
import { getRangePickerDefaultProps } from '#/utils';
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
import ChannelSelect from '#/views/im/manager/components/ChannelSelect.vue';
|
import { ChannelSelect } from '#/views/im/manager/channel/list/components';
|
||||||
|
|
||||||
/** 新增/修改的表单 */
|
/** 新增/修改的表单 */
|
||||||
export function useFormSchema(): VbenFormSchema[] {
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ import { useVbenModal } from '@vben/common-ui';
|
||||||
import { Form, FormItem, message, Radio, RadioGroup } from 'ant-design-vue';
|
import { Form, FormItem, message, Radio, RadioGroup } from 'ant-design-vue';
|
||||||
|
|
||||||
import { sendManagerChannelMessage } from '#/api/im/manager/channel/message';
|
import { sendManagerChannelMessage } from '#/api/im/manager/channel/message';
|
||||||
import ChannelSelect from '#/views/im/manager/components/ChannelSelect.vue';
|
import { ChannelSelect } from '#/views/im/manager/channel/list/components';
|
||||||
import MaterialSelect from '#/views/im/manager/components/MaterialSelect.vue';
|
import { MaterialSelect } from '#/views/im/manager/channel/material/components';
|
||||||
import UserMultiSelect from '#/views/im/manager/components/UserMultiSelect.vue';
|
import { UserSelect } from '#/views/system/user/components';
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
|
@ -89,8 +89,9 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem v-if="formData.receiverUserType === 'users'" label="接收用户" required>
|
<FormItem v-if="formData.receiverUserType === 'users'" label="接收用户" required>
|
||||||
<UserMultiSelect
|
<UserSelect
|
||||||
v-model="formData.receiverUserIds"
|
v-model="formData.receiverUserIds"
|
||||||
|
multiple
|
||||||
placeholder="请选择接收用户"
|
placeholder="请选择接收用户"
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { ImManagerGroupApi } from '#/api/im/manager/group';
|
|
||||||
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
import { Select } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { getManagerGroupPage } from '#/api/im/manager/group';
|
|
||||||
|
|
||||||
defineOptions({ name: 'ImManagerGroupSelect' });
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
allowClear?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
modelValue?: number;
|
|
||||||
placeholder?: string;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
allowClear: true,
|
|
||||||
disabled: false,
|
|
||||||
modelValue: undefined,
|
|
||||||
placeholder: '请选择群',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: number | undefined];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const groupList = ref<ImManagerGroupApi.Group[]>([]);
|
|
||||||
|
|
||||||
const value = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (val) => emit('update:modelValue', val),
|
|
||||||
});
|
|
||||||
|
|
||||||
const options = computed(() =>
|
|
||||||
groupList.value.map((item) => ({
|
|
||||||
label: `${item.name} (${item.id})`,
|
|
||||||
value: item.id,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
/** 加载群列表 */
|
|
||||||
async function loadGroupList() {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const data = await getManagerGroupPage({
|
|
||||||
pageNo: 1,
|
|
||||||
pageSize: 100,
|
|
||||||
});
|
|
||||||
groupList.value = data.list || [];
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadGroupList);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Select
|
|
||||||
v-model:value="value"
|
|
||||||
:allow-clear="allowClear"
|
|
||||||
:disabled="disabled"
|
|
||||||
:loading="loading"
|
|
||||||
:options="options"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
class="w-full"
|
|
||||||
show-search
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { SystemUserApi } from '#/api/system/user';
|
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
|
||||||
|
|
||||||
import { Input, Tag } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { getUser } from '#/api/system/user';
|
|
||||||
import UserSelectDialog from '#/views/system/user/components/select-dialog.vue';
|
|
||||||
|
|
||||||
defineOptions({ name: 'ImManagerUserMultiSelect' });
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
disabled?: boolean
|
|
||||||
modelValue?: number[]
|
|
||||||
placeholder?: string
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
disabled: false,
|
|
||||||
modelValue: () => [],
|
|
||||||
placeholder: '请选择用户',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
change: [rows: SystemUserApi.User[]];
|
|
||||||
'update:modelValue': [value: number[]];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const dialogRef = ref<InstanceType<typeof UserSelectDialog>>();
|
|
||||||
const selectedRows = ref<SystemUserApi.User[]>([]);
|
|
||||||
|
|
||||||
const displayText = computed(() => {
|
|
||||||
if (selectedRows.value.length === 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return selectedRows.value
|
|
||||||
.map((item) => item.nickname || item.username || item.id)
|
|
||||||
.join(',');
|
|
||||||
});
|
|
||||||
|
|
||||||
/** 回显已选用户 */
|
|
||||||
async function resolveSelectedUsers(ids: number[]) {
|
|
||||||
const knownMap = new Map(selectedRows.value.map((item) => [item.id, item]));
|
|
||||||
const list: SystemUserApi.User[] = [];
|
|
||||||
for (const id of ids) {
|
|
||||||
const cached = knownMap.get(id);
|
|
||||||
if (cached) {
|
|
||||||
list.push(cached);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
list.push(await getUser(id));
|
|
||||||
}
|
|
||||||
selectedRows.value = list;
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(ids) => {
|
|
||||||
void resolveSelectedUsers(ids || []);
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
/** 打开用户选择弹窗 */
|
|
||||||
function handleClick() {
|
|
||||||
if (props.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dialogRef.value?.open(props.modelValue || [], { multiple: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 弹窗选中回调 */
|
|
||||||
function handleSelected(rows: SystemUserApi.User[]) {
|
|
||||||
selectedRows.value = rows;
|
|
||||||
emit(
|
|
||||||
'update:modelValue',
|
|
||||||
rows.map((item) => item.id!),
|
|
||||||
);
|
|
||||||
emit('change', rows);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="w-full">
|
|
||||||
<Input
|
|
||||||
:disabled="disabled"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:value="displayText"
|
|
||||||
readonly
|
|
||||||
@click="handleClick"
|
|
||||||
>
|
|
||||||
<template #suffix>
|
|
||||||
<IconifyIcon class="size-4" icon="lucide:search" />
|
|
||||||
</template>
|
|
||||||
</Input>
|
|
||||||
<div v-if="selectedRows.length > 0" class="mt-2 flex flex-wrap gap-1">
|
|
||||||
<Tag v-for="item in selectedRows" :key="item.id">
|
|
||||||
{{ item.nickname || item.username || item.id }}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
<UserSelectDialog ref="dialogRef" @selected="handleSelected" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { getRangePickerDefaultProps } from '#/utils';
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
import UserSelect from '#/views/system/user/components/select.vue';
|
import { UserSelect } from '#/views/system/user/components';
|
||||||
|
|
||||||
/** 好友关系搜索表单 */
|
/** 好友关系搜索表单 */
|
||||||
export function useFriendGridFormSchema(): VbenFormSchema[] {
|
export function useFriendGridFormSchema(): VbenFormSchema[] {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as GroupSelectDialog } from './select-dialog.vue';
|
||||||
|
export { default as GroupSelect } from './select.vue';
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { ImManagerGroupApi } from '#/api/im/manager/group';
|
||||||
|
|
||||||
|
import { nextTick, ref } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { Button, message, Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
getManagerGroup,
|
||||||
|
getManagerGroupPage,
|
||||||
|
} from '#/api/im/manager/group';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
selected: [rows: ImManagerGroupApi.Group[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const open = ref(false); // 弹窗是否打开
|
||||||
|
const multiple = ref(false); // 是否多选
|
||||||
|
const selectedRows = ref<ImManagerGroupApi.Group[]>([]); // 已选群列表
|
||||||
|
const preSelectedIds = ref<number[]>([]); // 预选群编号列表
|
||||||
|
|
||||||
|
/** 群选择弹窗的搜索表单 */
|
||||||
|
function useGroupSelectGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '群名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
placeholder: '请输入群名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '群状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.IM_GROUP_STATUS, 'number'),
|
||||||
|
placeholder: '请选择群状态',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群选择弹窗的字段 */
|
||||||
|
function useGroupSelectGridColumns(
|
||||||
|
isMultiple = false,
|
||||||
|
): VxeTableGridOptions<ImManagerGroupApi.Group>['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: isMultiple ? 'checkbox' : 'radio',
|
||||||
|
width: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '编号',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '群名称',
|
||||||
|
minWidth: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'ownerNickname',
|
||||||
|
title: '群主',
|
||||||
|
minWidth: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'memberCount',
|
||||||
|
title: '成员数',
|
||||||
|
width: 90,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '群状态',
|
||||||
|
width: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.IM_GROUP_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取多选记录,包含 VXE reserve 跨页记录 */
|
||||||
|
function getMultipleSelectedRows() {
|
||||||
|
const selectedMap = new Map<number, ImManagerGroupApi.Group>();
|
||||||
|
const records = [
|
||||||
|
...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
|
||||||
|
...(gridApi.grid.getCheckboxRecords?.() ?? []),
|
||||||
|
] as ImManagerGroupApi.Group[];
|
||||||
|
records.forEach((row) => {
|
||||||
|
selectedMap.set(row.id, row);
|
||||||
|
});
|
||||||
|
return [...selectedMap.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理多选勾选变化 */
|
||||||
|
function handleCheckboxSelectChange() {
|
||||||
|
selectedRows.value = getMultipleSelectedRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理单选切换 */
|
||||||
|
function handleRadioChange(row: ImManagerGroupApi.Group) {
|
||||||
|
selectedRows.value = [row];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 多选模式下切换行勾选 */
|
||||||
|
async function toggleMultipleRow(row: ImManagerGroupApi.Group) {
|
||||||
|
const selected = gridApi.grid.isCheckedByCheckboxRow(row);
|
||||||
|
await gridApi.grid.setCheckboxRow(row, !selected);
|
||||||
|
selectedRows.value = getMultipleSelectedRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理行双击:单选直接确认,多选切换勾选 */
|
||||||
|
async function handleCellDblclick({ row }: { row: ImManagerGroupApi.Group }) {
|
||||||
|
if (multiple.value) {
|
||||||
|
await toggleMultipleRow(row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedRows.value = [row];
|
||||||
|
await gridApi.grid.setRadioRow(row);
|
||||||
|
handleConfirm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 回显预选群 */
|
||||||
|
async function applyPreSelection() {
|
||||||
|
if (preSelectedIds.value.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rows = gridApi.grid.getData() as ImManagerGroupApi.Group[];
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!preSelectedIds.value.includes(row.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (multiple.value) {
|
||||||
|
await gridApi.grid.setCheckboxRow(row, true);
|
||||||
|
} else {
|
||||||
|
await gridApi.grid.setRadioRow(row);
|
||||||
|
selectedRows.value = [row];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (multiple.value) {
|
||||||
|
selectedRows.value = getMultipleSelectedRows();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetId = preSelectedIds.value[0];
|
||||||
|
if (targetId) {
|
||||||
|
selectedRows.value = [await getManagerGroup(targetId)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGroupSelectGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGroupSelectGridColumns(false),
|
||||||
|
height: 520,
|
||||||
|
keepSource: true,
|
||||||
|
checkboxConfig: {
|
||||||
|
highlight: true,
|
||||||
|
range: true,
|
||||||
|
reserve: true,
|
||||||
|
},
|
||||||
|
radioConfig: {
|
||||||
|
highlight: true,
|
||||||
|
trigger: 'row',
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getManagerGroupPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<ImManagerGroupApi.Group>,
|
||||||
|
gridEvents: {
|
||||||
|
cellDblclick: handleCellDblclick,
|
||||||
|
checkboxAll: handleCheckboxSelectChange,
|
||||||
|
checkboxChange: handleCheckboxSelectChange,
|
||||||
|
radioChange: ({ row }: { row: ImManagerGroupApi.Group }) => {
|
||||||
|
handleRadioChange(row);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 重置查询和选择状态 */
|
||||||
|
async function resetQueryState() {
|
||||||
|
selectedRows.value = [];
|
||||||
|
await gridApi.grid.clearCheckboxRow();
|
||||||
|
await gridApi.grid.clearCheckboxReserve();
|
||||||
|
await gridApi.grid.clearRadioRow();
|
||||||
|
await gridApi.formApi.resetForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开群选择弹窗 */
|
||||||
|
async function openModal(
|
||||||
|
selectedIds?: number[],
|
||||||
|
options?: { multiple?: boolean },
|
||||||
|
) {
|
||||||
|
open.value = true;
|
||||||
|
multiple.value = options?.multiple ?? false;
|
||||||
|
preSelectedIds.value = selectedIds || [];
|
||||||
|
await nextTick();
|
||||||
|
gridApi.setGridOptions({
|
||||||
|
columns: useGroupSelectGridColumns(multiple.value),
|
||||||
|
});
|
||||||
|
await resetQueryState();
|
||||||
|
await gridApi.query();
|
||||||
|
await nextTick();
|
||||||
|
await applyPreSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭群选择弹窗 */
|
||||||
|
function closeModal() {
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确认选择群 */
|
||||||
|
function handleConfirm() {
|
||||||
|
const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
|
||||||
|
if (rows.length === 0) {
|
||||||
|
message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('selected', multiple.value ? rows : [rows[0]!]);
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open: openModal });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-model:open="open"
|
||||||
|
title="群选择"
|
||||||
|
width="70%"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
@ok="handleConfirm"
|
||||||
|
@cancel="closeModal"
|
||||||
|
>
|
||||||
|
<Grid table-title="群列表" />
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="closeModal">取消</Button>
|
||||||
|
<Button type="primary" @click="handleConfirm">确定</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ImManagerGroupApi } from '#/api/im/manager/group';
|
||||||
|
|
||||||
|
import { computed, ref, useAttrs, watch } from 'vue';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Input, Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getManagerGroup } from '#/api/im/manager/group';
|
||||||
|
|
||||||
|
import GroupSelectDialog from './select-dialog.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImManagerGroupSelect', inheritAttrs: false });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
allowClear?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
modelValue?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
allowClear: true,
|
||||||
|
disabled: false,
|
||||||
|
modelValue: undefined,
|
||||||
|
placeholder: '请选择群',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
change: [item: ImManagerGroupApi.Group | undefined];
|
||||||
|
'update:modelValue': [value: number | undefined];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const attrs = useAttrs();
|
||||||
|
const dialogRef = ref<InstanceType<typeof GroupSelectDialog>>(); // 群选择弹窗
|
||||||
|
const hovering = ref(false); // 鼠标是否悬停
|
||||||
|
const selectedItem = ref<ImManagerGroupApi.Group>(); // 已选群
|
||||||
|
|
||||||
|
/** 输入框显示文本 */
|
||||||
|
const displayLabel = computed(() => selectedItem.value?.name ?? '');
|
||||||
|
|
||||||
|
/** 是否显示清除图标 */
|
||||||
|
const showClear = computed(() => {
|
||||||
|
return props.allowClear && !props.disabled && hovering.value && props.modelValue != null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 根据编号查询群信息(用于编辑回显) */
|
||||||
|
async function resolveItemById(id: number | undefined) {
|
||||||
|
if (id == null) {
|
||||||
|
selectedItem.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedItem.value?.id === id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedItem.value = await getManagerGroup(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.modelValue, resolveItemById, { immediate: true });
|
||||||
|
|
||||||
|
/** 清空已选群 */
|
||||||
|
function clearSelected() {
|
||||||
|
selectedItem.value = undefined;
|
||||||
|
emit('update:modelValue', undefined);
|
||||||
|
emit('change', undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开群选择弹窗 */
|
||||||
|
function handleClick(event: MouseEvent) {
|
||||||
|
if (props.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (showClear.value && target.closest('.ant-input-suffix')) {
|
||||||
|
event.stopPropagation();
|
||||||
|
clearSelected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dialogRef.value?.open(props.modelValue == null ? [] : [props.modelValue]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 弹窗选中回调 */
|
||||||
|
function handleSelected(rows: ImManagerGroupApi.Group[]) {
|
||||||
|
const item = rows[0];
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedItem.value = item;
|
||||||
|
emit('update:modelValue', item.id);
|
||||||
|
emit('change', item);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-bind="attrs"
|
||||||
|
class="w-full"
|
||||||
|
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
|
||||||
|
@click="handleClick"
|
||||||
|
@mouseenter="hovering = true"
|
||||||
|
@mouseleave="hovering = false"
|
||||||
|
>
|
||||||
|
<Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
|
||||||
|
<template #title>
|
||||||
|
<div v-if="selectedItem" class="leading-6">
|
||||||
|
<div>群名称:{{ selectedItem.name || '-' }}</div>
|
||||||
|
<div>群主:{{ selectedItem.ownerNickname || '-' }}</div>
|
||||||
|
<div>成员数:{{ selectedItem.memberCount ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Input
|
||||||
|
:disabled="disabled"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:value="displayLabel"
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<IconifyIcon
|
||||||
|
class="size-4"
|
||||||
|
:icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Input>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<GroupSelectDialog ref="dialogRef" @selected="handleSelected" />
|
||||||
|
</template>
|
||||||
|
|
@ -7,8 +7,8 @@ import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { getRangePickerDefaultProps } from '#/utils';
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
import GroupSelect from '#/views/im/manager/components/GroupSelect.vue';
|
import { GroupSelect } from '#/views/im/manager/group/components';
|
||||||
import UserSelect from '#/views/system/user/components/select.vue';
|
import { UserSelect } from '#/views/system/user/components';
|
||||||
|
|
||||||
/** 群搜索表单 */
|
/** 群搜索表单 */
|
||||||
export function useGroupGridFormSchema(): VbenFormSchema[] {
|
export function useGroupGridFormSchema(): VbenFormSchema[] {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { formatFileSize, openSafeUrl } from '@vben/utils';
|
||||||
|
|
||||||
import { Image } from 'ant-design-vue';
|
import { Image } from 'ant-design-vue';
|
||||||
|
|
||||||
import CardLineLabel from '#/views/im/home/components/card/CardLineLabel.vue';
|
import { CardLineLabel } from '#/views/im/home/components/card';
|
||||||
import { MESSAGE_MERGE_PREVIEW_LINES } from '#/views/im/utils/config';
|
import { MESSAGE_MERGE_PREVIEW_LINES } from '#/views/im/utils/config';
|
||||||
import {
|
import {
|
||||||
ImContentType,
|
ImContentType,
|
||||||
|
|
@ -7,8 +7,8 @@ import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { getRangePickerDefaultProps } from '#/utils';
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
import GroupSelect from '#/views/im/manager/components/GroupSelect.vue';
|
import { GroupSelect } from '#/views/im/manager/group/components';
|
||||||
import UserSelect from '#/views/system/user/components/select.vue';
|
import { UserSelect } from '#/views/system/user/components';
|
||||||
|
|
||||||
/** 私聊消息搜索表单 */
|
/** 私聊消息搜索表单 */
|
||||||
export function usePrivateGridFormSchema(): VbenFormSchema[] {
|
export function usePrivateGridFormSchema(): VbenFormSchema[] {
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ import {
|
||||||
IM_AT_ALL_USER_ID,
|
IM_AT_ALL_USER_ID,
|
||||||
} from '#/views/im/utils/constants';
|
} from '#/views/im/utils/constants';
|
||||||
|
|
||||||
|
import { MessageContentPreview } from '..';
|
||||||
import { useGroupGridColumns, useGroupGridFormSchema } from '../data';
|
import { useGroupGridColumns, useGroupGridFormSchema } from '../data';
|
||||||
import MessageContentPreview from '../MessageContentPreview.vue';
|
|
||||||
import Detail from './modules/detail.vue';
|
import Detail from './modules/detail.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'ImGroupMessage' });
|
defineOptions({ name: 'ImGroupMessage' });
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import {
|
||||||
IM_AT_ALL_USER_ID,
|
IM_AT_ALL_USER_ID,
|
||||||
} from '#/views/im/utils/constants';
|
} from '#/views/im/utils/constants';
|
||||||
|
|
||||||
import MessageContentPreview from '../../MessageContentPreview.vue';
|
import { MessageContentPreview } from '../..';
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const detail = ref<ImManagerGroupMessageApi.GroupMessage>({} as ImManagerGroupMessageApi.GroupMessage);
|
const detail = ref<ImManagerGroupMessageApi.GroupMessage>({} as ImManagerGroupMessageApi.GroupMessage);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as MessageContentPreview } from './content-preview.vue';
|
||||||
|
|
@ -12,11 +12,11 @@ import { getManagerPrivateMessagePage } from '#/api/im/manager/message/private';
|
||||||
import { formatUserLabel } from '#/views/im/manager/utils/format';
|
import { formatUserLabel } from '#/views/im/manager/utils/format';
|
||||||
import { MESSAGE_PRIVATE_READ_ENABLED } from '#/views/im/utils/config';
|
import { MESSAGE_PRIVATE_READ_ENABLED } from '#/views/im/utils/config';
|
||||||
|
|
||||||
|
import { MessageContentPreview } from '..';
|
||||||
import {
|
import {
|
||||||
usePrivateGridColumns,
|
usePrivateGridColumns,
|
||||||
usePrivateGridFormSchema,
|
usePrivateGridFormSchema,
|
||||||
} from '../data';
|
} from '../data';
|
||||||
import MessageContentPreview from '../MessageContentPreview.vue';
|
|
||||||
import Detail from './modules/detail.vue';
|
import Detail from './modules/detail.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'ImPrivateMessage' });
|
defineOptions({ name: 'ImPrivateMessage' });
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue