feat: expand IM admin controls

This commit is contained in:
richjohnrambo
2026-08-21 15:09:39 +08:00
parent c3676f9744
commit 543c7b624e
29 changed files with 1308 additions and 39 deletions
+57
View File
@@ -0,0 +1,57 @@
<template>
<n-modal
v-model:show="showModal"
preset="dialog"
:show-icon="false"
:title="'登录设备 ' + userId"
:mask-closable="false"
:style="{ width: dialogWidth }"
>
<BasicTable
ref="actionRef"
:columns="deviceColumns"
:request="loadDataTable"
:row-key="(row) => row.deviceId"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
/>
<template #action>
<n-space>
<n-button @click="showModal = false">关闭</n-button>
</n-space>
</template>
</n-modal>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { BasicTable } from '@/components/Table';
import { Devices } from '@/api/imUser';
import { deviceColumns, State } from './model';
import { adaModalWidth, adaTableScrollX } from '@/utils/hotgo';
const showModal = ref(false);
const actionRef = ref();
const tenantId = ref(0);
const userId = ref('');
const dialogWidth = computed(() => adaModalWidth(1080));
const scrollX = computed(() => adaTableScrollX(deviceColumns, 0));
const loadDataTable = async (res) => {
if (!tenantId.value || !userId.value) {
return { list: [], page: 1, pageCount: 0, totalCount: 0 };
}
return await Devices({ tenantId: tenantId.value, userId: userId.value, ...res });
};
function openModal(record: State) {
tenantId.value = record.tenantId;
userId.value = record.userId;
showModal.value = true;
setTimeout(() => {
actionRef.value?.reload();
});
}
defineExpose({ openModal });
</script>
+37 -4
View File
@@ -1,7 +1,7 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="IM用户管理"></n-card>
<n-card :bordered="false" title="IM用户管理" />
</div>
<n-card :bordered="false" class="proCard">
<BasicForm
@@ -22,30 +22,34 @@
/>
</n-card>
<ViewModal ref="viewRef" />
<DevicesModal ref="devicesRef" />
<ResetPasswordModal ref="resetPasswordRef" @reload="reloadTable" />
</div>
</template>
<script lang="ts" setup>
import { computed, h, onMounted, reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { useUserStore } from '@/store/modules/user';
import { List } from '@/api/imUser';
import { List, RevokeSessions } from '@/api/imUser';
import { columns, loadOptions, loadTenantOptions, schemas, State, tenantOptions } from './model';
import { adaTableScrollX } from '@/utils/hotgo';
import ViewModal from './view.vue';
import DevicesModal from './DevicesModal.vue';
import ResetPasswordModal from './ResetPasswordModal.vue';
const message = useMessage();
const dialog = useDialog();
const { hasPermission } = usePermission();
const userStore = useUserStore();
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
const actionRef = ref();
const searchFormRef = ref<any>({});
const viewRef = ref();
const devicesRef = ref();
const resetPasswordRef = ref();
const tenantSchema = schemas.value.find((item) => item.field === 'tenantId');
@@ -54,7 +58,7 @@
}
const actionColumn = reactive({
width: 220,
width: 330,
title: '操作',
key: 'action',
fixed: 'right',
@@ -67,6 +71,17 @@
onClick: handleView.bind(null, record),
auth: ['/imUser/view'],
},
{
label: '设备',
onClick: handleDevices.bind(null, record),
auth: ['/imUser/devices'],
},
{
label: '撤销会话',
type: 'error',
onClick: handleRevokeSessions.bind(null, record),
auth: ['/imUser/revokeSessions'],
},
{
label: '重置密码',
type: 'warning',
@@ -107,6 +122,24 @@
viewRef.value.openModal(record);
}
function handleDevices(record: State) {
devicesRef.value.openModal(record);
}
function handleRevokeSessions(record: State) {
dialog.warning({
title: '撤销用户会话',
content: '将递增该用户认证版本,并通知 IM 服务清退在线会话,是否确认?',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await RevokeSessions({ tenantId: record.tenantId, userId: record.userId });
message.success('会话撤销事件已提交');
reloadTable();
},
});
}
function handleResetPassword(record: State) {
resetPasswordRef.value.openModal(record);
}
+60 -2
View File
@@ -1,11 +1,16 @@
import { computed, ref } from 'vue';
import { computed, h, ref } from 'vue';
import { NSwitch } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { List as TenantList } from '@/api/imTenant';
import { Status } from '@/api/imUser';
import { usePermission } from '@/hooks/web/usePermission';
const dict = useDictStore();
const { hasPermission } = usePermission();
const $message = window['$message'];
export const tenantOptions = ref<any[]>([]);
@@ -43,6 +48,21 @@ export class State {
}
}
export class DeviceState {
public tenantId = 0;
public tenantName = '';
public userId = '';
public deviceId = '';
public platform = '';
public lang = '';
public lastSeen = '';
public pushProvider = '';
public pushPlatform = '';
public pushAppVersion = '';
public pushTokenConfigured = false;
public pushUpdatedAt = '';
}
export function newState(state: State | Record<string, any> | null): State {
if (state !== null) {
if (state instanceof State) {
@@ -159,7 +179,25 @@ export const columns = [
align: 'left',
width: 110,
render(row: State) {
return dict.getLabel('ImUserStateOptions', row.state);
if (row.state === 20) {
return dict.getLabel('ImUserStateOptions', row.state);
}
return h(NSwitch, {
value: row.state === 0,
checked: '正常',
unchecked: '停用',
disabled: !hasPermission(['/imUser/status']),
onUpdateValue: async (value) => {
const oldState = row.state;
row.state = value ? 0 : 10;
try {
await Status({ tenantId: row.tenantId, userId: row.userId, state: row.state });
$message.success('操作成功');
} catch {
row.state = oldState;
}
},
});
},
},
{
@@ -200,6 +238,26 @@ export const columns = [
},
];
export const deviceColumns = [
{ title: '设备ID', key: 'deviceId', align: 'left', width: 240 },
{ title: '平台', key: 'platform', align: 'left', width: 120 },
{ title: '语言', key: 'lang', align: 'left', width: 90 },
{ title: '最后活跃', key: 'lastSeen', align: 'left', width: 180 },
{ title: 'Push服务商', key: 'pushProvider', align: 'left', width: 120 },
{ title: 'Push平台', key: 'pushPlatform', align: 'left', width: 120 },
{ title: 'App版本', key: 'pushAppVersion', align: 'left', width: 120 },
{
title: 'Push Token',
key: 'pushTokenConfigured',
align: 'left',
width: 120,
render(row: DeviceState) {
return row.pushTokenConfigured ? '已配置' : '未配置';
},
},
{ title: 'Push更新时间', key: 'pushUpdatedAt', align: 'left', width: 180 },
];
// 加载字典数据选项
export function loadOptions() {
dict.loadOptions(['ImUserStateOptions']);