Files
im_manager/web/src/views/imGroup/governance.vue
T

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>