feat: expand IM admin controls
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
import { http, jumpExport } from '@/utils/http/axios';
|
||||
|
||||
// 获取IM审计日志列表
|
||||
export function List(params) {
|
||||
@@ -17,3 +17,8 @@ export function View(params) {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 导出IM审计日志
|
||||
export function Export(params) {
|
||||
jumpExport('/imAuditLog/export', params);
|
||||
}
|
||||
|
||||
@@ -35,3 +35,36 @@ export function View(params) {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM租户用量和健康状态
|
||||
export function Usage(params) {
|
||||
return http.request({
|
||||
url: '/imTenant/usage',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function DeletionCheck(params) {
|
||||
return http.request({
|
||||
url: '/imTenant/deletionCheck',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function EnterDeleting(params) {
|
||||
return http.request({
|
||||
url: '/imTenant/enterDeleting',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function Restore(params) {
|
||||
return http.request({
|
||||
url: '/imTenant/restore',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,3 +26,30 @@ export function ResetPassword(params) {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 撤销IM用户会话
|
||||
export function RevokeSessions(params) {
|
||||
return http.request({
|
||||
url: '/imUser/revokeSessions',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新IM用户状态
|
||||
export function Status(params) {
|
||||
return http.request({
|
||||
url: '/imUser/status',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM用户设备列表
|
||||
export function Devices(params) {
|
||||
return http.request({
|
||||
url: '/imUser/devices',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,13 +22,23 @@
|
||||
>
|
||||
<template #tableTitle>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleCreate(false)" v-if="hasPermission(['/imInvite/create'])">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="handleCreate(false)"
|
||||
v-if="hasPermission(['/imInvite/create'])"
|
||||
>
|
||||
添加
|
||||
</n-button>
|
||||
<n-button type="primary" @click="handleCreate(true)" v-if="hasPermission(['/imInvite/batchCreate'])">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="handleCreate(true)"
|
||||
v-if="hasPermission(['/imInvite/batchCreate'])"
|
||||
>
|
||||
批量生成
|
||||
</n-button>
|
||||
<n-button @click="handleExport" v-if="hasPermission(['/imInvite/export'])">导出</n-button>
|
||||
<n-button @click="handleExport" v-if="hasPermission(['/imInvite/export'])">
|
||||
导出
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
@@ -45,7 +55,7 @@
|
||||
import { BasicForm, useForm } from '@/components/Form/index';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { Export, List, Revoke } from '@/api/im/registrationInvite';
|
||||
import { BatchRevoke, Export, List, Revoke } from '@/api/im/registrationInvite';
|
||||
import {
|
||||
columns,
|
||||
loadOptions,
|
||||
@@ -88,7 +98,7 @@
|
||||
);
|
||||
|
||||
const actionColumn = reactive({
|
||||
width: 220,
|
||||
width: 300,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
@@ -109,6 +119,13 @@
|
||||
auth: ['/imInvite/revoke'],
|
||||
ifShow: () => record.status === 'active',
|
||||
},
|
||||
{
|
||||
label: '撤销批次',
|
||||
type: 'error',
|
||||
onClick: handleBatchRevoke.bind(null, record),
|
||||
auth: ['/imInvite/batchRevoke'],
|
||||
ifShow: () => !!record.batchId && record.status === 'active',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
@@ -165,6 +182,23 @@
|
||||
});
|
||||
}
|
||||
|
||||
function handleBatchRevoke(record: State) {
|
||||
dialog.warning({
|
||||
title: '确认撤销批次',
|
||||
content: `将撤销批次 ${record.batchId} 下仍有效的邀请码,已过期或已撤销的邀请码不受影响,是否确认?`,
|
||||
positiveText: '确认',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await BatchRevoke({
|
||||
tenantId: Number(record.tenantId || selectedTenantId()),
|
||||
batchId: record.batchId,
|
||||
});
|
||||
message.success('批次撤销成功');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
const params = { ...searchFormRef.value?.formModel };
|
||||
const response: any = await Export(params);
|
||||
|
||||
@@ -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
|
||||
@@ -19,7 +19,23 @@
|
||||
:actionColumn="actionColumn"
|
||||
:scroll-x="scrollX"
|
||||
:resizeHeightOffset="-10000"
|
||||
/>
|
||||
>
|
||||
<template #tableTitle>
|
||||
<n-button
|
||||
type="primary"
|
||||
class="min-left-space"
|
||||
@click="handleExport"
|
||||
v-if="hasPermission(['/imAuditLog/export'])"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ExportOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
导出
|
||||
</n-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</n-card>
|
||||
<ViewModal ref="viewRef" />
|
||||
</div>
|
||||
@@ -30,16 +46,27 @@
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { BasicForm, useForm } from '@/components/Form/index';
|
||||
import { List } from '@/api/imAuditLog';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { List, Export } from '@/api/imAuditLog';
|
||||
import { columns, loadTenantOptions, schemas, State, tenantOptions } from './model';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import { ExportOutlined } from '@vicons/antd';
|
||||
import ViewModal from './view.vue';
|
||||
|
||||
const message = useMessage();
|
||||
const { hasPermission } = usePermission();
|
||||
const userStore = useUserStore();
|
||||
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
|
||||
const actionRef = ref();
|
||||
const searchFormRef = ref<any>({});
|
||||
const viewRef = ref();
|
||||
|
||||
const tenantSchema = schemas.value.find((item) => item.field === 'tenantId');
|
||||
if (tenantSchema) {
|
||||
tenantSchema.ifShow = () => isPlatformAdmin.value;
|
||||
}
|
||||
|
||||
const actionColumn = reactive({
|
||||
width: 100,
|
||||
title: '操作',
|
||||
@@ -48,7 +75,13 @@
|
||||
render(record: State) {
|
||||
return h(TableAction as any, {
|
||||
style: 'button',
|
||||
actions: [{ label: '详情', onClick: () => viewRef.value.openModal(record), auth: ['/imAuditLog/view'] }],
|
||||
actions: [
|
||||
{
|
||||
label: '详情',
|
||||
onClick: () => viewRef.value.openModal(record),
|
||||
auth: ['/imAuditLog/view'],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -63,7 +96,8 @@
|
||||
|
||||
const loadDataTable = async (res) => {
|
||||
const params = { ...searchFormRef.value?.formModel, ...res };
|
||||
if (!params.tenantId) return { list: [], page: 1, pageCount: 0, totalCount: 0 };
|
||||
if (isPlatformAdmin.value && !params.tenantId)
|
||||
return { list: [], page: 1, pageCount: 0, totalCount: 0 };
|
||||
return await List(params);
|
||||
};
|
||||
|
||||
@@ -73,19 +107,31 @@
|
||||
|
||||
async function handleReset() {
|
||||
const defaults: Recordable = {};
|
||||
if (tenantOptions.value.length > 0) defaults.tenantId = tenantOptions.value[0].value;
|
||||
if (isPlatformAdmin.value && tenantOptions.value.length > 0)
|
||||
defaults.tenantId = tenantOptions.value[0].value;
|
||||
await setFieldsValue(defaults);
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTenantOptions();
|
||||
if (tenantOptions.value.length > 0) {
|
||||
await setFieldsValue({ tenantId: tenantOptions.value[0].value });
|
||||
reloadTable();
|
||||
} else {
|
||||
message.warning('暂无可用租户');
|
||||
function handleExport() {
|
||||
const params = { ...searchFormRef.value?.formModel };
|
||||
if (isPlatformAdmin.value && !params.tenantId) {
|
||||
message.warning('请选择租户');
|
||||
return;
|
||||
}
|
||||
Export(params);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (isPlatformAdmin.value) {
|
||||
await loadTenantOptions();
|
||||
if (tenantOptions.value.length > 0) {
|
||||
await setFieldsValue({ tenantId: tenantOptions.value[0].value });
|
||||
} else {
|
||||
message.warning('暂无可用租户');
|
||||
}
|
||||
}
|
||||
reloadTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
preset="dialog"
|
||||
:show-icon="false"
|
||||
:title="'删除前检查 #' + current.id"
|
||||
:mask-closable="false"
|
||||
:style="{ width: dialogWidth }"
|
||||
>
|
||||
<n-spin :show="loading" description="请稍候...">
|
||||
<n-alert :type="detail.canEnterDeleting ? 'success' : 'warning'" class="mb-4">
|
||||
{{
|
||||
detail.canEnterDeleting ? '检查通过,可进入删除流程。' : '检查未通过,不能进入删除流程。'
|
||||
}}
|
||||
</n-alert>
|
||||
<n-descriptions label-placement="left" bordered :column="settingStore.isMobile ? 1 : 2">
|
||||
<n-descriptions-item label="租户">{{ detail.name || current.name }}</n-descriptions-item>
|
||||
<n-descriptions-item label="编码">{{ detail.code || current.code }}</n-descriptions-item>
|
||||
<n-descriptions-item label="用户数">{{ detail.userCount }}</n-descriptions-item>
|
||||
<n-descriptions-item label="群数">{{ detail.groupCount }}</n-descriptions-item>
|
||||
<n-descriptions-item label="待处理Outbox">{{ detail.pendingOutbox }}</n-descriptions-item>
|
||||
<n-descriptions-item v-if="detail.blockers?.length" label="阻断项" :span="2">
|
||||
<n-code :code="detail.blockers.join('\n')" language="text" word-wrap />
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item v-if="detail.warnings?.length" label="警告" :span="2">
|
||||
<n-code :code="detail.warnings.join('\n')" language="text" word-wrap />
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-spin>
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button @click="load">重新检查</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
:disabled="!detail.canEnterDeleting"
|
||||
:loading="submitLoading"
|
||||
@click="enterDeleting"
|
||||
>
|
||||
进入删除流程
|
||||
</n-button>
|
||||
<n-button @click="showModal = false">关闭</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { DeletionCheck, EnterDeleting } from '@/api/imTenant';
|
||||
import { newState, State } from './model';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
import { adaModalWidth } from '@/utils/hotgo';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
const message = useMessage();
|
||||
const settingStore = useProjectSettingStore();
|
||||
const showModal = ref(false);
|
||||
const loading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const current = ref<State>(newState(null));
|
||||
const dialogWidth = computed(() => adaModalWidth(840));
|
||||
const detail = reactive({
|
||||
id: 0,
|
||||
code: '',
|
||||
name: '',
|
||||
canEnterDeleting: false,
|
||||
blockers: [] as string[],
|
||||
warnings: [] as string[],
|
||||
userCount: 0,
|
||||
groupCount: 0,
|
||||
pendingOutbox: 0,
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
Object.assign(detail, await DeletionCheck({ id: current.value.id }));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function enterDeleting() {
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
await EnterDeleting({ id: current.value.id });
|
||||
message.success('已进入删除流程');
|
||||
showModal.value = false;
|
||||
emit('reload');
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openModal(state: State) {
|
||||
current.value = newState(state);
|
||||
showModal.value = true;
|
||||
load();
|
||||
}
|
||||
|
||||
defineExpose({ openModal });
|
||||
</script>
|
||||
@@ -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
|
||||
@@ -37,7 +37,9 @@
|
||||
</template>
|
||||
</BasicTable>
|
||||
</n-card>
|
||||
<Edit ref="editRef" @reloadTable="reloadTable" />
|
||||
<Edit ref="editRef" @reload-table="reloadTable" />
|
||||
<UsageModal ref="usageRef" />
|
||||
<DeletionCheckModal ref="deletionCheckRef" @reload="reloadTable" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -48,11 +50,13 @@
|
||||
import { BasicForm, useForm } from '@/components/Form/index';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { List, Status } from '@/api/imTenant';
|
||||
import { List, Restore, Status } from '@/api/imTenant';
|
||||
import { PlusOutlined } from '@vicons/antd';
|
||||
import { columns, loadOptions, schemas, State } from './model';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import Edit from './edit.vue';
|
||||
import UsageModal from './usage.vue';
|
||||
import DeletionCheckModal from './deletionCheck.vue';
|
||||
|
||||
const dict = useDictStore();
|
||||
const message = useMessage();
|
||||
@@ -60,9 +64,11 @@
|
||||
const actionRef = ref();
|
||||
const searchFormRef = ref<any>({});
|
||||
const editRef = ref();
|
||||
const usageRef = ref();
|
||||
const deletionCheckRef = ref();
|
||||
|
||||
const actionColumn = reactive({
|
||||
width: 180,
|
||||
width: 240,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
@@ -75,6 +81,11 @@
|
||||
onClick: handleEdit.bind(null, record),
|
||||
auth: ['/imTenant/edit'],
|
||||
},
|
||||
{
|
||||
label: '用量',
|
||||
onClick: handleUsage.bind(null, record),
|
||||
auth: ['/imTenant/usage'],
|
||||
},
|
||||
{
|
||||
label: '停用',
|
||||
onClick: handleStatus.bind(null, record, 2),
|
||||
@@ -84,9 +95,22 @@
|
||||
{
|
||||
label: '启用',
|
||||
onClick: handleStatus.bind(null, record, 1),
|
||||
ifShow: () => record.state !== 1,
|
||||
ifShow: () => ![1, 3, 4].includes(record.state),
|
||||
auth: ['/imTenant/status'],
|
||||
},
|
||||
{
|
||||
label: '恢复',
|
||||
onClick: handleRestore.bind(null, record),
|
||||
ifShow: () => [2, 3].includes(record.state),
|
||||
auth: ['/imTenant/restore'],
|
||||
},
|
||||
{
|
||||
label: '删除检查',
|
||||
type: 'error',
|
||||
onClick: handleDeletionCheck.bind(null, record),
|
||||
ifShow: () => record.state !== 4,
|
||||
auth: ['/imTenant/deletionCheck'],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
@@ -122,6 +146,21 @@
|
||||
editRef.value.openModal(record);
|
||||
}
|
||||
|
||||
function handleUsage(record: State) {
|
||||
usageRef.value.openModal(record);
|
||||
}
|
||||
|
||||
function handleDeletionCheck(record: State) {
|
||||
deletionCheckRef.value.openModal(record);
|
||||
}
|
||||
|
||||
function handleRestore(record: State) {
|
||||
Restore({ id: record.id }).then((_res) => {
|
||||
message.success('恢复成功');
|
||||
reloadTable();
|
||||
});
|
||||
}
|
||||
|
||||
// 修改状态
|
||||
function handleStatus(record: Recordable, state: number) {
|
||||
Status({ id: record.id, state }).then((_res) => {
|
||||
|
||||
@@ -34,6 +34,25 @@ export class State {
|
||||
}
|
||||
}
|
||||
|
||||
export class UsageState {
|
||||
public id = 0;
|
||||
public code = '';
|
||||
public name = '';
|
||||
public dbName = '';
|
||||
public dbDsnConfigured = false;
|
||||
public dbReachable = false;
|
||||
public dbCheckError = '';
|
||||
public schemaVersion = '';
|
||||
public userCount = 0;
|
||||
public activeUserCount = 0;
|
||||
public groupCount = 0;
|
||||
public deviceCount = 0;
|
||||
public pushTokenCount = 0;
|
||||
public inviteUseCount = 0;
|
||||
public outboxPendingCount = 0;
|
||||
public warnings: string[] = [];
|
||||
}
|
||||
|
||||
export function newState(state: State | Record<string, any> | null): State {
|
||||
if (state !== null) {
|
||||
if (state instanceof State) {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
preset="dialog"
|
||||
:show-icon="false"
|
||||
:title="'租户健康与用量 #' + current.id"
|
||||
:mask-closable="false"
|
||||
:style="{ width: dialogWidth }"
|
||||
>
|
||||
<n-spin :show="loading" description="请稍候...">
|
||||
<n-descriptions label-placement="left" bordered :column="settingStore.isMobile ? 1 : 2">
|
||||
<n-descriptions-item label="租户">{{ usage.name || current.name }}</n-descriptions-item>
|
||||
<n-descriptions-item label="租户编码">{{ usage.code || current.code }}</n-descriptions-item>
|
||||
<n-descriptions-item label="业务库">{{ usage.dbName || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="独立连接串">
|
||||
{{ usage.dbDsnConfigured ? '已配置' : '未配置' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="业务库状态">
|
||||
<n-tag :type="usage.dbReachable ? 'success' : 'error'" :bordered="false">
|
||||
{{ usage.dbReachable ? '可达' : '不可达' }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="Schema版本">
|
||||
{{ usage.schemaVersion || '-' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="用户数">{{ usage.userCount }}</n-descriptions-item>
|
||||
<n-descriptions-item label="正常用户数">{{ usage.activeUserCount }}</n-descriptions-item>
|
||||
<n-descriptions-item label="群数">{{ usage.groupCount }}</n-descriptions-item>
|
||||
<n-descriptions-item label="设备数">{{ usage.deviceCount }}</n-descriptions-item>
|
||||
<n-descriptions-item label="Push Token">{{ usage.pushTokenCount }}</n-descriptions-item>
|
||||
<n-descriptions-item label="邀请码使用">{{ usage.inviteUseCount }}</n-descriptions-item>
|
||||
<n-descriptions-item label="待处理Outbox">
|
||||
{{ usage.outboxPendingCount }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item v-if="usage.dbCheckError" label="连接错误" :span="2">
|
||||
{{ usage.dbCheckError }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item v-if="usage.warnings?.length" label="检查警告" :span="2">
|
||||
<n-code :code="usage.warnings.join('\n')" language="text" word-wrap />
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-spin>
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button @click="load">刷新</n-button>
|
||||
<n-button @click="showModal = false">关闭</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { Usage } from '@/api/imTenant';
|
||||
import { newState, State, UsageState } from './model';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
import { adaModalWidth } from '@/utils/hotgo';
|
||||
|
||||
const settingStore = useProjectSettingStore();
|
||||
const showModal = ref(false);
|
||||
const loading = ref(false);
|
||||
const current = ref<State>(newState(null));
|
||||
const usage = ref<UsageState>(new UsageState());
|
||||
const dialogWidth = computed(() => adaModalWidth(900));
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
usage.value = Object.assign(new UsageState(), await Usage({ id: current.value.id }));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openModal(state: State) {
|
||||
current.value = newState(state);
|
||||
usage.value = new UsageState();
|
||||
showModal.value = true;
|
||||
load();
|
||||
}
|
||||
|
||||
defineExpose({ openModal });
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user