新增IM聊天群管理接口,包括群聊详情、添加成员、移除成员、修改群名称、转让群主、退出群聊及解散群聊功能,提升群聊管理体验。
This commit is contained in:
@@ -131,6 +131,9 @@
|
||||
</button>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleSettingsMenuClick">
|
||||
<!--update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群聊设置入口------------->
|
||||
<a-menu-item v-if="isGroupChat" key="groupSettings">群设置</a-menu-item>
|
||||
<!--update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群聊设置入口----------->
|
||||
<a-menu-item key="chatSettings">聊天设置</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
@@ -214,6 +217,10 @@
|
||||
<ImChatSettingsModal @register="registerSettingsModal" @saved="onChatSettingsSaved" />
|
||||
<ImPageListPickModal @register="registerListPickModal" @confirm="handleListRowsSend" />
|
||||
<ImCreateGroupModal @register="registerCreateGroupModal" @success="handleGroupCreated" />
|
||||
<!--update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉------------->
|
||||
<ImGroupSettingDrawer @register="registerGroupSettingDrawer" @changed="handleGroupSettingChanged" @exited="handleGroupExited" />
|
||||
<!--update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉----------->
|
||||
|
||||
<a-modal :open="previewVisible" :footer="null" width="720px" @cancel="closeImagePreview">
|
||||
<img alt="图片预览" style="width: 100%" :src="previewImageUrl" />
|
||||
</a-modal>
|
||||
@@ -243,6 +250,10 @@
|
||||
import ImPageListPickModal from './ImPageListPickModal.vue';
|
||||
import ImBizRecordMessageContent from './ImBizRecordMessageContent.vue';
|
||||
import ImCreateGroupModal from './ImCreateGroupModal.vue';
|
||||
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉-----------
|
||||
import ImGroupSettingDrawer from './ImGroupSettingDrawer.vue';
|
||||
import { useDrawer } from '/@/components/Drawer';
|
||||
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉-----------
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { buildImBizRecordPayload, parseImBizRecordPayload, serializeImBizRecordPayload } from './imBizRecordMessage';
|
||||
@@ -499,6 +510,9 @@
|
||||
const [registerListPickModal, { openModal: openListPickModal }] = useModal();
|
||||
const [registerCreateGroupModal, { openModal: openCreateGroupModal }] = useModal();
|
||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】点击功能名气泡选择列表明细发送-----------
|
||||
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉-----------
|
||||
const [registerGroupSettingDrawer, { openDrawer: openGroupSettingDrawer, closeDrawer: closeGroupSettingDrawer }] = useDrawer();
|
||||
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉-----------
|
||||
const defaultHistoryDays = ref(getImDefaultHistoryDays());
|
||||
const currentUserId = computed(() => userStore.getUserInfo?.id || '');
|
||||
const deptLabel = computed(() => {
|
||||
@@ -715,9 +729,60 @@
|
||||
function handleSettingsMenuClick({ key }: { key: string }) {
|
||||
if (key === 'chatSettings') {
|
||||
openSettingsModal(true, {});
|
||||
return;
|
||||
}
|
||||
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】打开群设置抽屉-----------
|
||||
if (key === 'groupSettings') {
|
||||
if (!activeGroup.value?.conversationId) {
|
||||
return;
|
||||
}
|
||||
openGroupSettingDrawer(true, { conversationId: activeGroup.value.conversationId });
|
||||
}
|
||||
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】打开群设置抽屉-----------
|
||||
}
|
||||
|
||||
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置变更/退出回调-----------
|
||||
/** 群信息变更(改名/加人/踢人/转让):同步标题、成员数,并刷新群列表 */
|
||||
async function handleGroupSettingChanged(payload: { conversationId: string; groupName?: string; memberCount?: number }) {
|
||||
if (!payload?.conversationId) {
|
||||
return;
|
||||
}
|
||||
patchGroup(payload.conversationId, {
|
||||
groupName: payload.groupName,
|
||||
memberCount: payload.memberCount,
|
||||
}, { moveToTop: false });
|
||||
if (activeGroup.value?.conversationId === payload.conversationId) {
|
||||
activeGroup.value = {
|
||||
...activeGroup.value,
|
||||
groupName: payload.groupName ?? activeGroup.value.groupName,
|
||||
memberCount: payload.memberCount ?? activeGroup.value.memberCount,
|
||||
};
|
||||
}
|
||||
// 后台静默刷新群列表,保证成员数等准确
|
||||
await loadGroups(true);
|
||||
}
|
||||
|
||||
/** 退出或解散群聊:从列表移除,若为当前会话则清空聊天区 */
|
||||
async function handleGroupExited(conversationId: string) {
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
groupList.value = groupList.value.filter((item) => item.conversationId !== conversationId);
|
||||
resetGroupUnread(conversationId);
|
||||
syncAllUnread();
|
||||
if (activeGroup.value?.conversationId === conversationId) {
|
||||
activeGroup.value = null;
|
||||
activeChatType.value = '';
|
||||
activeConversationId.value = '';
|
||||
setImActiveConversationId('');
|
||||
messageList.value = [];
|
||||
draft.value = '';
|
||||
clearImActiveSession();
|
||||
}
|
||||
closeGroupSettingDrawer();
|
||||
}
|
||||
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置变更/退出回调-----------
|
||||
|
||||
function onChatSettingsSaved() {
|
||||
defaultHistoryDays.value = getImDefaultHistoryDays();
|
||||
if (activeConversationId.value) {
|
||||
|
||||
164
jeecgboot-vue3/src/views/system/im/ImGroupAddMemberModal.vue
Normal file
164
jeecgboot-vue3/src/views/system/im/ImGroupAddMemberModal.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<!--update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-添加群成员弹窗----------->
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" title="添加群成员" :width="520" @register="registerModal" @ok="handleSubmit">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="选择成员" required>
|
||||
<a-input
|
||||
v-model:value="memberKeyword"
|
||||
placeholder="搜索同事"
|
||||
allow-clear
|
||||
size="small"
|
||||
class="member-search"
|
||||
@pressEnter="loadMembers"
|
||||
/>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="member-list">
|
||||
<a-checkbox-group v-model:value="checkedMemberIds" class="member-checkbox-group">
|
||||
<div v-for="item in filteredMembers" :key="item.id" class="member-row">
|
||||
<a-checkbox :value="item.id">
|
||||
<div class="member-info">
|
||||
<a-avatar :size="28" :src="getAvatarUrl(item.avatar)">
|
||||
{{ (item.realname || item.username || '?').slice(0, 1) }}
|
||||
</a-avatar>
|
||||
<span class="member-name">{{ item.realname || item.username }}</span>
|
||||
</div>
|
||||
</a-checkbox>
|
||||
</div>
|
||||
</a-checkbox-group>
|
||||
<a-empty v-if="!filteredMembers.length" description="暂无可添加的同事" />
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { addGroupMembers, fetchDeptMembers } from './im.api';
|
||||
|
||||
defineOptions({ name: 'ImGroupAddMemberModal' });
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const memberKeyword = ref('');
|
||||
const checkedMemberIds = ref<string[]>([]);
|
||||
const members = ref<Recordable[]>([]);
|
||||
const loading = ref(false);
|
||||
const conversationId = ref('');
|
||||
// 已在群成员(用于过滤候选)
|
||||
const existMemberIds = ref<string[]>([]);
|
||||
|
||||
const filteredMembers = computed(() => {
|
||||
const keyword = memberKeyword.value.trim().toLowerCase();
|
||||
if (!keyword) {
|
||||
return members.value;
|
||||
}
|
||||
return members.value.filter((item) => {
|
||||
const name = `${item.realname || ''}${item.username || ''}`.toLowerCase();
|
||||
return name.includes(keyword);
|
||||
});
|
||||
});
|
||||
|
||||
/** 过滤掉当前用户与已在群成员 */
|
||||
function resolveSelectableMembers(source: Recordable[]) {
|
||||
const currentUserId = userStore.getUserInfo?.id || '';
|
||||
const excludeSet = new Set([currentUserId, ...existMemberIds.value]);
|
||||
return source.filter((item) => item.id && !excludeSet.has(item.id));
|
||||
}
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(
|
||||
async (data?: { conversationId?: string; existMemberIds?: string[] }) => {
|
||||
memberKeyword.value = '';
|
||||
checkedMemberIds.value = [];
|
||||
conversationId.value = data?.conversationId || '';
|
||||
existMemberIds.value = data?.existMemberIds || [];
|
||||
setModalProps({ confirmLoading: false });
|
||||
members.value = [];
|
||||
await loadMembers();
|
||||
},
|
||||
);
|
||||
|
||||
function getAvatarUrl(avatar?: string) {
|
||||
return avatar ? getFileAccessHttpUrl(avatar) : '';
|
||||
}
|
||||
|
||||
async function loadMembers() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const list = ((await fetchDeptMembers(memberKeyword.value || undefined)) || []) as Recordable[];
|
||||
members.value = resolveSelectableMembers(list);
|
||||
} catch {
|
||||
createMessage.error('加载同事列表失败,请稍后重试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!conversationId.value) {
|
||||
return;
|
||||
}
|
||||
if (!checkedMemberIds.value.length) {
|
||||
createMessage.warning('请至少选择1名同事');
|
||||
return;
|
||||
}
|
||||
setModalProps({ confirmLoading: true });
|
||||
try {
|
||||
await addGroupMembers({
|
||||
conversationId: conversationId.value,
|
||||
memberUserIds: checkedMemberIds.value,
|
||||
});
|
||||
createMessage.success('添加成功');
|
||||
emit('success');
|
||||
closeModal();
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.member-search {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.member-checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.member-row {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.member-info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
<!--update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-添加群成员弹窗----------->
|
||||
473
jeecgboot-vue3/src/views/system/im/ImGroupSettingDrawer.vue
Normal file
473
jeecgboot-vue3/src/views/system/im/ImGroupSettingDrawer.vue
Normal file
@@ -0,0 +1,473 @@
|
||||
<!--update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉----------->
|
||||
<template>
|
||||
<BasicDrawer v-bind="$attrs" title="群设置" :width="360" @register="registerDrawer">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="group-setting">
|
||||
<!-- 群成员宫格 -->
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
群成员
|
||||
<span class="member-count">({{ detail.memberCount || 0 }})</span>
|
||||
</div>
|
||||
<div class="member-grid">
|
||||
<div v-for="m in detail.members" :key="m.userId" class="member-cell">
|
||||
<div class="member-avatar-wrap">
|
||||
<a-avatar :size="44" :src="getAvatarUrl(m.avatar)">
|
||||
{{ (m.realname || m.username || '?').slice(0, 1) }}
|
||||
</a-avatar>
|
||||
<span v-if="m.owner" class="owner-tag">群主</span>
|
||||
<span
|
||||
v-if="removeMode && !m.owner"
|
||||
class="remove-badge"
|
||||
@click="handleRemoveMember(m)"
|
||||
>
|
||||
<Icon icon="ant-design:minus-outlined" />
|
||||
</span>
|
||||
</div>
|
||||
<span class="member-name">{{ m.realname || m.username }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 添加成员(所有成员可见) -->
|
||||
<div class="member-cell">
|
||||
<div class="member-action-btn" @click="handleOpenAddMember">
|
||||
<Icon icon="ant-design:plus-outlined" />
|
||||
</div>
|
||||
<span class="member-name">添加</span>
|
||||
</div>
|
||||
|
||||
<!-- 移除成员(仅群主可见) -->
|
||||
<div v-if="detail.owner" class="member-cell">
|
||||
<div :class="['member-action-btn', { active: removeMode }]" @click="toggleRemoveMode">
|
||||
<Icon icon="ant-design:minus-outlined" />
|
||||
</div>
|
||||
<span class="member-name">{{ removeMode ? '完成' : '移除' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 群名称 -->
|
||||
<div class="section">
|
||||
<div class="setting-row" :class="{ 'is-clickable': detail.owner }" @click="handleEditName">
|
||||
<span class="row-label">群聊名称</span>
|
||||
<span class="row-value">
|
||||
{{ detail.groupName || '未命名群聊' }}
|
||||
<Icon v-if="detail.owner" icon="ant-design:right-outlined" class="row-arrow" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 群主管理 -->
|
||||
<div v-if="detail.owner" class="section">
|
||||
<div class="setting-row is-clickable" @click="handleOpenTransfer">
|
||||
<span class="row-label">转让群主</span>
|
||||
<Icon icon="ant-design:right-outlined" class="row-arrow" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<div class="footer-actions">
|
||||
<a-button v-if="detail.owner" danger block @click="handleDismiss">解散群聊</a-button>
|
||||
<a-button v-else danger block @click="handleQuit">退出群聊</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<!-- 添加成员弹窗 -->
|
||||
<ImGroupAddMemberModal @register="registerAddMemberModal" @success="onMembersChanged" />
|
||||
|
||||
<!-- 修改群名称弹窗 -->
|
||||
<a-modal v-model:open="nameModalVisible" title="修改群名称" :confirm-loading="nameSaving" @ok="submitRename">
|
||||
<a-input v-model:value="editingName" placeholder="请输入群名称" :maxlength="30" show-count />
|
||||
</a-modal>
|
||||
|
||||
<!-- 转让群主弹窗 -->
|
||||
<a-modal v-model:open="transferModalVisible" title="转让群主" :confirm-loading="transferSaving" @ok="submitTransfer">
|
||||
<div class="transfer-tip">选择一名群成员作为新群主,转让后你将不再是群主。</div>
|
||||
<a-radio-group v-model:value="transferTargetId" class="transfer-list">
|
||||
<a-radio v-for="m in transferCandidates" :key="m.userId" :value="m.userId" class="transfer-item">
|
||||
<a-avatar :size="28" :src="getAvatarUrl(m.avatar)">
|
||||
{{ (m.realname || m.username || '?').slice(0, 1) }}
|
||||
</a-avatar>
|
||||
<span class="transfer-name">{{ m.realname || m.username }}</span>
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
<a-empty v-if="!transferCandidates.length" description="暂无其他群成员" />
|
||||
</a-modal>
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
import {
|
||||
fetchGroupDetail,
|
||||
removeGroupMember,
|
||||
renameGroup,
|
||||
transferGroupOwner,
|
||||
quitGroup,
|
||||
dismissGroup,
|
||||
type ImGroupDetail,
|
||||
type ImGroupMember,
|
||||
} from './im.api';
|
||||
import ImGroupAddMemberModal from './ImGroupAddMemberModal.vue';
|
||||
|
||||
defineOptions({ name: 'ImGroupSettingDrawer' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 群信息变更(改名/加人/踢人/转让),父组件据此刷新会话 */
|
||||
(e: 'changed', payload: { conversationId: string; groupName?: string; memberCount?: number }): void;
|
||||
/** 退出或解散群聊,父组件据此移除会话并清理当前会话 */
|
||||
(e: 'exited', conversationId: string): void;
|
||||
}>();
|
||||
|
||||
const { createMessage, createConfirm } = useMessage();
|
||||
|
||||
const loading = ref(false);
|
||||
const conversationId = ref('');
|
||||
const detail = reactive<ImGroupDetail>({
|
||||
conversationId: '',
|
||||
groupName: '',
|
||||
ownerId: '',
|
||||
memberCount: 0,
|
||||
owner: false,
|
||||
members: [],
|
||||
});
|
||||
|
||||
const removeMode = ref(false);
|
||||
|
||||
// 改名
|
||||
const nameModalVisible = ref(false);
|
||||
const editingName = ref('');
|
||||
const nameSaving = ref(false);
|
||||
|
||||
// 转让群主
|
||||
const transferModalVisible = ref(false);
|
||||
const transferTargetId = ref('');
|
||||
const transferSaving = ref(false);
|
||||
const transferCandidates = computed(() => detail.members.filter((m) => !m.owner));
|
||||
|
||||
const [registerAddMemberModal, { openModal: openAddMemberModal }] = useModal();
|
||||
|
||||
const [registerDrawer, { setDrawerProps }] = useDrawerInner(async (data?: { conversationId?: string }) => {
|
||||
removeMode.value = false;
|
||||
conversationId.value = data?.conversationId || '';
|
||||
if (!conversationId.value) {
|
||||
return;
|
||||
}
|
||||
setDrawerProps({ loading: false });
|
||||
await loadDetail();
|
||||
});
|
||||
|
||||
function getAvatarUrl(avatar?: string) {
|
||||
return avatar ? getFileAccessHttpUrl(avatar) : '';
|
||||
}
|
||||
|
||||
function applyDetail(data: ImGroupDetail) {
|
||||
detail.conversationId = data.conversationId;
|
||||
detail.groupName = data.groupName;
|
||||
detail.ownerId = data.ownerId;
|
||||
detail.memberCount = data.memberCount;
|
||||
detail.owner = data.owner;
|
||||
detail.members = data.members || [];
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await fetchGroupDetail(conversationId.value);
|
||||
applyDetail(data);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function notifyChanged() {
|
||||
emit('changed', {
|
||||
conversationId: conversationId.value,
|
||||
groupName: detail.groupName,
|
||||
memberCount: detail.memberCount,
|
||||
});
|
||||
}
|
||||
|
||||
function handleOpenAddMember() {
|
||||
openAddMemberModal(true, {
|
||||
conversationId: conversationId.value,
|
||||
existMemberIds: detail.members.map((m) => m.userId),
|
||||
});
|
||||
}
|
||||
|
||||
async function onMembersChanged() {
|
||||
await loadDetail();
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
function toggleRemoveMode() {
|
||||
removeMode.value = !removeMode.value;
|
||||
}
|
||||
|
||||
function handleRemoveMember(member: ImGroupMember) {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '移除成员',
|
||||
content: `确定将「${member.realname || member.username}」移出群聊吗?`,
|
||||
onOk: async () => {
|
||||
await removeGroupMember(conversationId.value, member.userId);
|
||||
createMessage.success('已移除');
|
||||
await loadDetail();
|
||||
notifyChanged();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditName() {
|
||||
if (!detail.owner) {
|
||||
return;
|
||||
}
|
||||
editingName.value = detail.groupName || '';
|
||||
nameModalVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitRename() {
|
||||
const name = editingName.value.trim();
|
||||
if (!name) {
|
||||
createMessage.warning('请输入群名称');
|
||||
return;
|
||||
}
|
||||
nameSaving.value = true;
|
||||
try {
|
||||
await renameGroup(conversationId.value, name);
|
||||
detail.groupName = name;
|
||||
nameModalVisible.value = false;
|
||||
createMessage.success('修改成功');
|
||||
notifyChanged();
|
||||
} finally {
|
||||
nameSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenTransfer() {
|
||||
transferTargetId.value = '';
|
||||
transferModalVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitTransfer() {
|
||||
if (!transferTargetId.value) {
|
||||
createMessage.warning('请选择新群主');
|
||||
return;
|
||||
}
|
||||
transferSaving.value = true;
|
||||
try {
|
||||
await transferGroupOwner(conversationId.value, transferTargetId.value);
|
||||
transferModalVisible.value = false;
|
||||
createMessage.success('转让成功');
|
||||
await loadDetail();
|
||||
notifyChanged();
|
||||
} finally {
|
||||
transferSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleQuit() {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '退出群聊',
|
||||
content: '退出后将不再接收该群消息,确定退出吗?',
|
||||
onOk: async () => {
|
||||
await quitGroup(conversationId.value);
|
||||
createMessage.success('已退出群聊');
|
||||
emit('exited', conversationId.value);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleDismiss() {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '解散群聊',
|
||||
content: '解散后群聊将被删除且无法恢复,确定解散吗?',
|
||||
onOk: async () => {
|
||||
await dismissGroup(conversationId.value);
|
||||
createMessage.success('群聊已解散');
|
||||
emit('exited', conversationId.value);
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.group-setting {
|
||||
padding: 4px 4px 16px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.member-count {
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
.member-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 8px;
|
||||
}
|
||||
|
||||
.member-cell {
|
||||
width: 56px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.member-avatar-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.owner-tag {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -4px;
|
||||
transform: translateX(-50%);
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
padding: 0 4px;
|
||||
color: #fff;
|
||||
background: #fa8c16;
|
||||
border-radius: 7px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.remove-badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.member-action-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
color: #999;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1677ff;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #ff4d4f;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 12px;
|
||||
color: #595959;
|
||||
max-width: 56px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 28px;
|
||||
|
||||
&.is-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.row-label {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.row-value {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.row-arrow {
|
||||
color: #c0c0c0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.transfer-tip {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.transfer-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.transfer-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
:deep(.ant-radio + span) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.transfer-name {
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
<!--update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉----------->
|
||||
@@ -8,6 +8,15 @@ enum Api {
|
||||
read = '/sys/im/chat/read',
|
||||
groups = '/sys/im/chat/groups',
|
||||
groupCreate = '/sys/im/chat/group/create',
|
||||
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置接口-----------
|
||||
groupDetail = '/sys/im/chat/group/detail',
|
||||
groupAddMembers = '/sys/im/chat/group/addMembers',
|
||||
groupRemoveMember = '/sys/im/chat/group/removeMember',
|
||||
groupRename = '/sys/im/chat/group/rename',
|
||||
groupTransfer = '/sys/im/chat/group/transfer',
|
||||
groupQuit = '/sys/im/chat/group/quit',
|
||||
groupDismiss = '/sys/im/chat/group/dismiss',
|
||||
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置接口-----------
|
||||
}
|
||||
|
||||
export interface ImGroupConversation {
|
||||
@@ -57,3 +66,51 @@ export const fetchGroups = () => defHttp.get<ImGroupConversation[]>({ url: Api.g
|
||||
|
||||
export const createGroupConversation = (data: { groupName: string; memberUserIds: string[] }) =>
|
||||
defHttp.post<ImGroupConversation>({ url: Api.groupCreate, data });
|
||||
|
||||
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置接口-----------
|
||||
export interface ImGroupMember {
|
||||
userId: string;
|
||||
realname?: string;
|
||||
username?: string;
|
||||
avatar?: string;
|
||||
owner?: boolean;
|
||||
}
|
||||
|
||||
export interface ImGroupDetail {
|
||||
conversationId: string;
|
||||
groupName?: string;
|
||||
ownerId?: string;
|
||||
memberCount?: number;
|
||||
/** 当前登录用户是否群主 */
|
||||
owner?: boolean;
|
||||
members: ImGroupMember[];
|
||||
}
|
||||
|
||||
/** 群聊详情(含成员列表) */
|
||||
export const fetchGroupDetail = (conversationId: string) =>
|
||||
defHttp.get<ImGroupDetail>({ url: Api.groupDetail, params: { conversationId } });
|
||||
|
||||
/** 添加群成员 */
|
||||
export const addGroupMembers = (data: { conversationId: string; memberUserIds: string[] }) =>
|
||||
defHttp.post<ImGroupConversation>({ url: Api.groupAddMembers, data });
|
||||
|
||||
/** 移除群成员(仅群主) */
|
||||
export const removeGroupMember = (conversationId: string, memberUserId: string) =>
|
||||
defHttp.post({ url: Api.groupRemoveMember, params: { conversationId, memberUserId } }, { joinParamsToUrl: true });
|
||||
|
||||
/** 修改群名称(仅群主) */
|
||||
export const renameGroup = (conversationId: string, groupName: string) =>
|
||||
defHttp.post<ImGroupConversation>({ url: Api.groupRename, params: { conversationId, groupName } }, { joinParamsToUrl: true });
|
||||
|
||||
/** 转让群主(仅群主) */
|
||||
export const transferGroupOwner = (conversationId: string, newOwnerId: string) =>
|
||||
defHttp.post({ url: Api.groupTransfer, params: { conversationId, newOwnerId } }, { joinParamsToUrl: true });
|
||||
|
||||
/** 退出群聊(非群主成员) */
|
||||
export const quitGroup = (conversationId: string) =>
|
||||
defHttp.post({ url: Api.groupQuit, params: { conversationId } }, { joinParamsToUrl: true });
|
||||
|
||||
/** 解散群聊(仅群主) */
|
||||
export const dismissGroup = (conversationId: string) =>
|
||||
defHttp.post({ url: Api.groupDismiss, params: { conversationId } }, { joinParamsToUrl: true });
|
||||
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置接口-----------
|
||||
|
||||
Reference in New Issue
Block a user