523 lines
16 KiB
Vue
523 lines
16 KiB
Vue
<template>
|
|
<div>
|
|
<n-modal
|
|
v-model:show="showModal"
|
|
:mask-closable="false"
|
|
:show-icon="false"
|
|
preset="dialog"
|
|
transform-origin="center"
|
|
:title="'群治理 ' + current.topic"
|
|
:style="{ width: dialogWidth }"
|
|
>
|
|
<n-tabs v-model:value="activeTab" type="line" animated>
|
|
<n-tab-pane name="settings" tab="治理配置">
|
|
<n-spin :show="loading" description="请稍候...">
|
|
<n-descriptions label-placement="left" bordered :column="settingStore.isMobile ? 1 : 2">
|
|
<n-descriptions-item label="群名称">{{ governance.name || '-' }}</n-descriptions-item>
|
|
<n-descriptions-item label="业务状态">{{ groupStatusLabel }}</n-descriptions-item>
|
|
<n-descriptions-item label="群主ID">{{
|
|
governance.ownerUserId || '-'
|
|
}}</n-descriptions-item>
|
|
<n-descriptions-item label="成员数">{{
|
|
governance.memberCount || '-'
|
|
}}</n-descriptions-item>
|
|
<n-descriptions-item label="入群审批">
|
|
<n-switch
|
|
v-model:value="governance.joinApproval"
|
|
:checked-value="1"
|
|
:unchecked-value="0"
|
|
:disabled="!canUpdateGovernance"
|
|
/>
|
|
</n-descriptions-item>
|
|
<n-descriptions-item label="全员禁言">
|
|
<n-switch
|
|
v-model:value="governance.muteAll"
|
|
:checked-value="1"
|
|
:unchecked-value="0"
|
|
:disabled="!canUpdateGovernance"
|
|
/>
|
|
</n-descriptions-item>
|
|
<n-descriptions-item label="成员上传文件">
|
|
<n-switch
|
|
v-model:value="governance.memberUploadFiles"
|
|
:checked-value="1"
|
|
:unchecked-value="0"
|
|
:disabled="!canUpdateGovernance"
|
|
/>
|
|
</n-descriptions-item>
|
|
<n-descriptions-item label="成员私聊">
|
|
<n-switch
|
|
v-model:value="governance.memberPrivateChat"
|
|
:checked-value="1"
|
|
:unchecked-value="0"
|
|
:disabled="!canUpdateGovernance"
|
|
/>
|
|
</n-descriptions-item>
|
|
<n-descriptions-item label="成员邀请">
|
|
<n-switch
|
|
v-model:value="governance.memberInvite"
|
|
:checked-value="1"
|
|
:unchecked-value="0"
|
|
:disabled="!canUpdateGovernance"
|
|
/>
|
|
</n-descriptions-item>
|
|
<n-descriptions-item label="最大成员数">
|
|
<n-input-number
|
|
v-model:value="governance.maxMemberCount"
|
|
:min="0"
|
|
:disabled="!canUpdateGovernance"
|
|
/>
|
|
</n-descriptions-item>
|
|
<n-descriptions-item label="最大管理员数">
|
|
<n-input-number
|
|
v-model:value="governance.maxAdminCount"
|
|
:min="0"
|
|
:disabled="!canUpdateGovernance"
|
|
/>
|
|
</n-descriptions-item>
|
|
<n-descriptions-item label="版本">{{
|
|
governance.version || '-'
|
|
}}</n-descriptions-item>
|
|
<n-descriptions-item label="创建人">{{
|
|
governance.createdBy || '-'
|
|
}}</n-descriptions-item>
|
|
<n-descriptions-item label="创建时间">{{
|
|
governance.createdAt || '-'
|
|
}}</n-descriptions-item>
|
|
<n-descriptions-item label="解散人">{{
|
|
governance.dissolvedBy || '-'
|
|
}}</n-descriptions-item>
|
|
<n-descriptions-item label="解散时间">{{
|
|
governance.dissolvedAt || '-'
|
|
}}</n-descriptions-item>
|
|
<n-descriptions-item label="简介" :span="2">{{
|
|
governance.description || '-'
|
|
}}</n-descriptions-item>
|
|
</n-descriptions>
|
|
</n-spin>
|
|
</n-tab-pane>
|
|
<n-tab-pane name="joinRequests" tab="入群申请">
|
|
<BasicTable
|
|
ref="joinRequestRef"
|
|
:columns="joinRequestColumns"
|
|
:request="joinRequestData"
|
|
:row-key="(row) => row.requestId"
|
|
:actionColumn="joinRequestActionColumn"
|
|
:scroll-x="joinRequestScrollX"
|
|
:resizeHeightOffset="-10000"
|
|
/>
|
|
</n-tab-pane>
|
|
<n-tab-pane name="files" tab="群文件">
|
|
<BasicTable
|
|
ref="fileRef"
|
|
:columns="groupFileColumns"
|
|
:request="fileData"
|
|
:row-key="(row) => row.groupFileId"
|
|
:actionColumn="fileActionColumn"
|
|
:scroll-x="fileScrollX"
|
|
:resizeHeightOffset="-10000"
|
|
/>
|
|
</n-tab-pane>
|
|
<n-tab-pane name="invitations" tab="群邀请">
|
|
<BasicTable
|
|
ref="invitationRef"
|
|
:columns="invitationColumns"
|
|
:request="invitationData"
|
|
:row-key="(row) => row.invitationId"
|
|
:scroll-x="invitationScrollX"
|
|
:resizeHeightOffset="-10000"
|
|
/>
|
|
</n-tab-pane>
|
|
<n-tab-pane name="announcements" tab="群公告">
|
|
<BasicTable
|
|
ref="announcementRef"
|
|
:columns="announcementColumns"
|
|
:request="announcementData"
|
|
:row-key="(row) => row.announcementId"
|
|
:scroll-x="announcementScrollX"
|
|
:resizeHeightOffset="-10000"
|
|
/>
|
|
</n-tab-pane>
|
|
<n-tab-pane name="mutes" tab="成员禁言">
|
|
<BasicTable
|
|
ref="muteRef"
|
|
:columns="muteColumns"
|
|
:request="muteData"
|
|
:row-key="(row) => row.topic + '-' + row.userId"
|
|
:actionColumn="muteActionColumn"
|
|
:scroll-x="muteScrollX"
|
|
:resizeHeightOffset="-10000"
|
|
/>
|
|
</n-tab-pane>
|
|
<n-tab-pane name="logs" tab="管理日志">
|
|
<BasicTable
|
|
ref="adminLogRef"
|
|
:columns="adminLogColumns"
|
|
:request="adminLogData"
|
|
:row-key="(row) => row.logId"
|
|
:scroll-x="adminLogScrollX"
|
|
:resizeHeightOffset="-10000"
|
|
/>
|
|
</n-tab-pane>
|
|
</n-tabs>
|
|
<template #action>
|
|
<n-space>
|
|
<n-button
|
|
v-if="activeTab === 'settings' && canUpdateGovernance"
|
|
type="primary"
|
|
:loading="submitLoading"
|
|
@click="handleSaveGovernance"
|
|
>
|
|
保存治理配置
|
|
</n-button>
|
|
<n-button
|
|
v-if="activeTab === 'settings' && canDissolve && governance.groupStatus !== 'dissolved'"
|
|
type="error"
|
|
:loading="submitLoading"
|
|
@click="handleDissolve"
|
|
>
|
|
解散群
|
|
</n-button>
|
|
<n-button @click="closeForm">关闭</n-button>
|
|
</n-space>
|
|
</template>
|
|
</n-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, h, nextTick, reactive, ref, watch } from 'vue';
|
|
import { useDialog, useMessage } from 'naive-ui';
|
|
import { useDictStore } from '@/store/modules/dict';
|
|
import { usePermission } from '@/hooks/web/usePermission';
|
|
import { BasicTable, TableAction } from '@/components/Table';
|
|
import {
|
|
AdminLogList,
|
|
AnnouncementList,
|
|
Dissolve,
|
|
FileDelete,
|
|
FileList,
|
|
Governance,
|
|
GovernanceUpdate,
|
|
InvitationList,
|
|
JoinRequestApprove,
|
|
JoinRequestList,
|
|
JoinRequestReject,
|
|
MuteList,
|
|
Unmute,
|
|
} from '@/api/imGroup';
|
|
import {
|
|
adminLogColumns,
|
|
announcementColumns,
|
|
GovernanceState,
|
|
groupFileColumns,
|
|
invitationColumns,
|
|
joinRequestColumns,
|
|
muteColumns,
|
|
newState,
|
|
State,
|
|
} from './model';
|
|
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
|
import { adaModalWidth, adaTableScrollX } from '@/utils/hotgo';
|
|
|
|
const settingStore = useProjectSettingStore();
|
|
const dict = useDictStore();
|
|
const message = useMessage();
|
|
const dialog = useDialog();
|
|
const { hasPermission } = usePermission();
|
|
const showModal = ref(false);
|
|
const loading = ref(false);
|
|
const submitLoading = ref(false);
|
|
const activeTab = ref('settings');
|
|
const current = ref<State>(newState(null));
|
|
const governance = ref<GovernanceState>(new GovernanceState());
|
|
const dialogWidth = computed(() => adaModalWidth(1120));
|
|
const canUpdateGovernance = computed(() => hasPermission(['/imGroup/governanceUpdate']));
|
|
const canDissolve = computed(() => hasPermission(['/imGroup/dissolve']));
|
|
const groupStatusLabel = computed(
|
|
() => dict.getLabel('ImGroupStatusOptions', governance.value.groupStatus) || '-'
|
|
);
|
|
|
|
const joinRequestRef = ref();
|
|
const fileRef = ref();
|
|
const invitationRef = ref();
|
|
const announcementRef = ref();
|
|
const muteRef = ref();
|
|
const adminLogRef = ref();
|
|
const joinRequestActionColumn = reactive({
|
|
width: 150,
|
|
title: '操作',
|
|
key: 'action',
|
|
fixed: 'right',
|
|
render(record) {
|
|
return h(TableAction as any, {
|
|
style: 'button',
|
|
actions: [
|
|
{
|
|
label: '通过',
|
|
type: 'primary',
|
|
ifShow: () => record.status === 'pending',
|
|
auth: ['/imGroup/joinRequestApprove'],
|
|
onClick: () => handleJoinRequestApprove(record),
|
|
},
|
|
{
|
|
label: '拒绝',
|
|
type: 'error',
|
|
ifShow: () => record.status === 'pending',
|
|
auth: ['/imGroup/joinRequestReject'],
|
|
onClick: () => handleJoinRequestReject(record),
|
|
},
|
|
],
|
|
});
|
|
},
|
|
});
|
|
const fileActionColumn = reactive({
|
|
width: 100,
|
|
title: '操作',
|
|
key: 'action',
|
|
fixed: 'right',
|
|
render(record) {
|
|
return h(TableAction as any, {
|
|
style: 'button',
|
|
actions: [
|
|
{
|
|
label: '删除',
|
|
type: 'error',
|
|
ifShow: () => record.status === 'active',
|
|
auth: ['/imGroup/fileDelete'],
|
|
onClick: () => handleFileDelete(record),
|
|
},
|
|
],
|
|
});
|
|
},
|
|
});
|
|
const muteActionColumn = reactive({
|
|
width: 110,
|
|
title: '操作',
|
|
key: 'action',
|
|
fixed: 'right',
|
|
render(record) {
|
|
return h(TableAction as any, {
|
|
style: 'button',
|
|
actions: [
|
|
{
|
|
label: '解除',
|
|
type: 'warning',
|
|
ifShow: () => record.status === 'active',
|
|
auth: ['/imGroup/unmute'],
|
|
onClick: () => handleUnmute(record),
|
|
},
|
|
],
|
|
});
|
|
},
|
|
});
|
|
|
|
const joinRequestScrollX = computed(() =>
|
|
adaTableScrollX(joinRequestColumns, joinRequestActionColumn.width)
|
|
);
|
|
const fileScrollX = computed(() => adaTableScrollX(groupFileColumns, fileActionColumn.width));
|
|
const invitationScrollX = computed(() => adaTableScrollX(invitationColumns, 0));
|
|
const announcementScrollX = computed(() => adaTableScrollX(announcementColumns, 0));
|
|
const muteScrollX = computed(() => adaTableScrollX(muteColumns, muteActionColumn.width));
|
|
const adminLogScrollX = computed(() => adaTableScrollX(adminLogColumns, 0));
|
|
|
|
function baseParams(res) {
|
|
return {
|
|
tenantId: current.value.tenantId,
|
|
topic: current.value.topic,
|
|
...res,
|
|
};
|
|
}
|
|
|
|
async function joinRequestData(res) {
|
|
return await JoinRequestList(baseParams(res));
|
|
}
|
|
|
|
async function fileData(res) {
|
|
return await FileList(baseParams(res));
|
|
}
|
|
|
|
async function invitationData(res) {
|
|
return await InvitationList(baseParams(res));
|
|
}
|
|
|
|
async function announcementData(res) {
|
|
return await AnnouncementList(baseParams(res));
|
|
}
|
|
|
|
async function muteData(res) {
|
|
return await MuteList(baseParams(res));
|
|
}
|
|
|
|
async function adminLogData(res) {
|
|
return await AdminLogList(baseParams(res));
|
|
}
|
|
|
|
function closeForm() {
|
|
showModal.value = false;
|
|
loading.value = false;
|
|
}
|
|
|
|
async function loadGovernance() {
|
|
loading.value = true;
|
|
try {
|
|
const res = await Governance({
|
|
tenantId: current.value.tenantId,
|
|
topic: current.value.topic,
|
|
});
|
|
governance.value = Object.assign(new GovernanceState(), res || {});
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function handleSaveGovernance() {
|
|
submitLoading.value = true;
|
|
try {
|
|
await GovernanceUpdate({
|
|
tenantId: current.value.tenantId,
|
|
topic: current.value.topic,
|
|
joinApproval: governance.value.joinApproval,
|
|
muteAll: governance.value.muteAll,
|
|
memberUploadFiles: governance.value.memberUploadFiles,
|
|
memberPrivateChat: governance.value.memberPrivateChat,
|
|
memberInvite: governance.value.memberInvite,
|
|
maxMemberCount: governance.value.maxMemberCount,
|
|
maxAdminCount: governance.value.maxAdminCount,
|
|
});
|
|
message.success('操作成功');
|
|
await loadGovernance();
|
|
} finally {
|
|
submitLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function handleDissolve() {
|
|
dialog.error({
|
|
title: '解散群',
|
|
content: '解散后该群将进入不可用状态,并写入群事件等待 IM 服务端同步,是否确认?',
|
|
positiveText: '确认',
|
|
negativeText: '取消',
|
|
onPositiveClick: async () => {
|
|
submitLoading.value = true;
|
|
try {
|
|
await Dissolve({
|
|
tenantId: current.value.tenantId,
|
|
topic: current.value.topic,
|
|
reason: '后台管理员解散',
|
|
});
|
|
message.success('操作成功');
|
|
await loadGovernance();
|
|
} finally {
|
|
submitLoading.value = false;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
function handleUnmute(record) {
|
|
dialog.warning({
|
|
title: '解除禁言',
|
|
content: '确定解除该成员禁言?',
|
|
positiveText: '确认',
|
|
negativeText: '取消',
|
|
onPositiveClick: async () => {
|
|
await Unmute({
|
|
tenantId: current.value.tenantId,
|
|
topic: current.value.topic,
|
|
userId: record.userId,
|
|
});
|
|
message.success('操作成功');
|
|
muteRef.value?.reload();
|
|
},
|
|
});
|
|
}
|
|
|
|
function handleJoinRequestApprove(record) {
|
|
dialog.success({
|
|
title: '通过入群申请',
|
|
content: '确定通过该入群申请?',
|
|
positiveText: '确认',
|
|
negativeText: '取消',
|
|
onPositiveClick: async () => {
|
|
await JoinRequestApprove({
|
|
tenantId: current.value.tenantId,
|
|
topic: current.value.topic,
|
|
requestId: record.requestId,
|
|
});
|
|
message.success('操作成功');
|
|
joinRequestRef.value?.reload();
|
|
},
|
|
});
|
|
}
|
|
|
|
function handleJoinRequestReject(record) {
|
|
dialog.error({
|
|
title: '拒绝入群申请',
|
|
content: '确定拒绝该入群申请?',
|
|
positiveText: '确认',
|
|
negativeText: '取消',
|
|
onPositiveClick: async () => {
|
|
await JoinRequestReject({
|
|
tenantId: current.value.tenantId,
|
|
topic: current.value.topic,
|
|
requestId: record.requestId,
|
|
rejectReason: '后台管理员拒绝',
|
|
});
|
|
message.success('操作成功');
|
|
joinRequestRef.value?.reload();
|
|
},
|
|
});
|
|
}
|
|
|
|
function handleFileDelete(record) {
|
|
dialog.error({
|
|
title: '删除群文件',
|
|
content: '删除后该群文件会被标记为已删除,并写入群事件等待 IM 服务端同步,是否确认?',
|
|
positiveText: '确认',
|
|
negativeText: '取消',
|
|
onPositiveClick: async () => {
|
|
await FileDelete({
|
|
tenantId: current.value.tenantId,
|
|
topic: current.value.topic,
|
|
groupFileId: record.groupFileId,
|
|
});
|
|
message.success('操作成功');
|
|
fileRef.value?.reload();
|
|
},
|
|
});
|
|
}
|
|
|
|
function reloadActiveTable() {
|
|
const refs = {
|
|
joinRequests: joinRequestRef,
|
|
files: fileRef,
|
|
invitations: invitationRef,
|
|
announcements: announcementRef,
|
|
mutes: muteRef,
|
|
logs: adminLogRef,
|
|
};
|
|
refs[activeTab.value]?.value?.reload();
|
|
}
|
|
|
|
function openModal(state: State) {
|
|
current.value = newState(state);
|
|
governance.value = new GovernanceState();
|
|
activeTab.value = 'settings';
|
|
showModal.value = true;
|
|
loadGovernance();
|
|
}
|
|
|
|
watch(activeTab, async (value) => {
|
|
if (!showModal.value || value === 'settings') return;
|
|
await nextTick();
|
|
reloadActiveTable();
|
|
});
|
|
|
|
defineExpose({
|
|
openModal,
|
|
});
|
|
</script>
|
|
|
|
<style lang="less"></style>
|