feat: add im kafka admin integration
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
// 获取IM Kafka死信列表
|
||||
export function List(params) {
|
||||
return http.request({
|
||||
url: '/imDeadLetter/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM Kafka死信详情
|
||||
export function View(params) {
|
||||
return http.request({
|
||||
url: '/imDeadLetter/view',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 重放IM Kafka死信
|
||||
export function Replay(params) {
|
||||
return http.request({
|
||||
url: '/imDeadLetter/replay',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 忽略IM Kafka死信
|
||||
export function Ignore(params) {
|
||||
return http.request({
|
||||
url: '/imDeadLetter/ignore',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 标记IM Kafka死信已处理
|
||||
export function Resolve(params) {
|
||||
return http.request({
|
||||
url: '/imDeadLetter/resolve',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
export function List(params) {
|
||||
return http.request({
|
||||
url: '/imKafkaAudit/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function View(params) {
|
||||
return http.request({
|
||||
url: '/imKafkaAudit/view',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
// 获取IM后台命令列表
|
||||
export function List(params) {
|
||||
return http.request({
|
||||
url: '/imKafkaCommand/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM后台命令详情
|
||||
export function View(params) {
|
||||
return http.request({
|
||||
url: '/imKafkaCommand/view',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 发送IM缓存失效测试事件
|
||||
export function CacheInvalidate(params) {
|
||||
return http.request({
|
||||
url: '/imKafkaCommand/cacheInvalidate',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 发送IM工作台测试事件
|
||||
export function WorkbenchEvent(params) {
|
||||
return http.request({
|
||||
url: '/imKafkaCommand/workbenchEvent',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
@@ -53,3 +53,35 @@ export function Devices(params) {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function Ban(params) {
|
||||
return http.request({ url: '/imUser/ban', method: 'POST', params });
|
||||
}
|
||||
|
||||
export function Unban(params) {
|
||||
return http.request({ url: '/imUser/unban', method: 'POST', params });
|
||||
}
|
||||
|
||||
export function Freeze(params) {
|
||||
return http.request({ url: '/imUser/freeze', method: 'POST', params });
|
||||
}
|
||||
|
||||
export function Unfreeze(params) {
|
||||
return http.request({ url: '/imUser/unfreeze', method: 'POST', params });
|
||||
}
|
||||
|
||||
export function DeviceKick(params) {
|
||||
return http.request({ url: '/imUser/deviceKick', method: 'POST', params });
|
||||
}
|
||||
|
||||
export function LogoutAll(params) {
|
||||
return http.request({ url: '/imUser/logoutAll', method: 'POST', params });
|
||||
}
|
||||
|
||||
export function AccountCancel(params) {
|
||||
return http.request({ url: '/imUser/accountCancel', method: 'POST', params });
|
||||
}
|
||||
|
||||
export function AccountDelete(params) {
|
||||
return http.request({ url: '/imUser/accountDelete', method: 'POST', params });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="n-layout-page-header">
|
||||
<n-card :bordered="false" title="IM Kafka死信"></n-card>
|
||||
</div>
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<BasicForm
|
||||
ref="searchFormRef"
|
||||
@register="register"
|
||||
@submit="reloadTable"
|
||||
@reset="reloadTable"
|
||||
@keyup.enter="reloadTable"
|
||||
/>
|
||||
<BasicTable
|
||||
ref="actionRef"
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
:row-key="(row) => row.deadId"
|
||||
:actionColumn="actionColumn"
|
||||
:scroll-x="scrollX"
|
||||
:resizeHeightOffset="-10000"
|
||||
/>
|
||||
</n-card>
|
||||
<ViewModal ref="viewRef" @reload="reloadTable" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { BasicForm, useForm } from '@/components/Form/index';
|
||||
import { List } from '@/api/imDeadLetter';
|
||||
import { columns, loadOptions, loadTenantOptions, schemas, State } from './model';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import ViewModal from './view.vue';
|
||||
|
||||
const actionRef = ref();
|
||||
const searchFormRef = ref<any>({});
|
||||
const viewRef = ref();
|
||||
const cursor = ref('');
|
||||
const hasMore = ref(false);
|
||||
|
||||
const actionColumn = reactive({
|
||||
width: 110,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render(record: State) {
|
||||
return h(TableAction as any, {
|
||||
style: 'button',
|
||||
actions: [{ label: '详情', onClick: () => viewRef.value.openModal(record, currentTenantId.value), auth: ['/imDeadLetter/view'] }],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
|
||||
const currentTenantId = computed(() => searchFormRef.value?.formModel?.tenantId || 0);
|
||||
|
||||
const [register] = useForm({
|
||||
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||
labelWidth: 90,
|
||||
schemas,
|
||||
});
|
||||
|
||||
const loadDataTable = async (res) => {
|
||||
if (!currentTenantId.value) {
|
||||
return { list: [], totalCount: 0 };
|
||||
}
|
||||
const params = { ...searchFormRef.value?.formModel, limit: res.pageSize || res.perPage || 50, cursor: cursor.value };
|
||||
const data = await List(params);
|
||||
hasMore.value = !!data?.hasMore;
|
||||
cursor.value = data?.nextCursor || '';
|
||||
return { list: data?.list || [], totalCount: data?.list?.length || 0 };
|
||||
};
|
||||
|
||||
function reloadTable() {
|
||||
cursor.value = '';
|
||||
hasMore.value = false;
|
||||
actionRef.value?.reload();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadOptions();
|
||||
await loadTenantOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,122 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { FormSchema } from '@/components/Form';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { List as TenantList } from '@/api/imTenant';
|
||||
|
||||
const dict = useDictStore();
|
||||
|
||||
export const tenantOptions = ref<any[]>([]);
|
||||
|
||||
export class State {
|
||||
public id = 0;
|
||||
public tenantId = 0;
|
||||
public deadId = '';
|
||||
public eventId = '';
|
||||
public parentEventId = '';
|
||||
public sourceTopic = '';
|
||||
public consumerGroup = '';
|
||||
public eventType = '';
|
||||
public bizType = '';
|
||||
public bizId = '';
|
||||
public retryCount = 0;
|
||||
public lastReason = '';
|
||||
public lastError = '';
|
||||
public status = '';
|
||||
public replayCount = 0;
|
||||
public createdAt = '';
|
||||
public updatedAt = '';
|
||||
public envelope = '';
|
||||
public envelopeRaw = '';
|
||||
|
||||
constructor(state?: Partial<State>) {
|
||||
if (state) Object.assign(this, state);
|
||||
}
|
||||
}
|
||||
|
||||
export function newState(state: State | Record<string, any> | null): State {
|
||||
if (state !== null) {
|
||||
if (state instanceof State) return cloneDeep(state);
|
||||
return new State(state);
|
||||
}
|
||||
return new State();
|
||||
}
|
||||
|
||||
export const schemas = ref<FormSchema[]>([
|
||||
{
|
||||
field: 'tenantId',
|
||||
component: 'NSelect',
|
||||
label: '租户',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
options: computed(() => tenantOptions.value),
|
||||
placeholder: '请选择租户',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
component: 'NSelect',
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
options: computed(() => dict.getOptions('ImKafkaDeadLetterStatusOptions')),
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'eventType',
|
||||
component: 'NInput',
|
||||
label: '事件类型',
|
||||
componentProps: { placeholder: '请输入事件类型' },
|
||||
},
|
||||
{
|
||||
field: 'bizType',
|
||||
component: 'NInput',
|
||||
label: '业务类型',
|
||||
componentProps: { placeholder: '请输入业务类型' },
|
||||
},
|
||||
{
|
||||
field: 'bizId',
|
||||
component: 'NInput',
|
||||
label: '业务ID',
|
||||
componentProps: { placeholder: '请输入业务ID' },
|
||||
},
|
||||
]);
|
||||
|
||||
export const columns = [
|
||||
{ title: 'ID', key: 'id', align: 'left', width: 90 },
|
||||
{ title: '死信ID', key: 'deadId', align: 'left', width: 220 },
|
||||
{ title: '事件ID', key: 'eventId', align: 'left', width: 220 },
|
||||
{ title: '来源Topic', key: 'sourceTopic', align: 'left', width: 150 },
|
||||
{ title: '消费组', key: 'consumerGroup', align: 'left', width: 160 },
|
||||
{ title: '事件类型', key: 'eventType', align: 'left', width: 190 },
|
||||
{ title: '业务类型', key: 'bizType', align: 'left', width: 130 },
|
||||
{ title: '业务ID', key: 'bizId', align: 'left', width: 180 },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
align: 'left',
|
||||
width: 120,
|
||||
render(row: State) {
|
||||
return dict.getLabel('ImKafkaDeadLetterStatusOptions', row.status);
|
||||
},
|
||||
},
|
||||
{ title: '重试', key: 'retryCount', align: 'left', width: 80 },
|
||||
{ title: '重放', key: 'replayCount', align: 'left', width: 80 },
|
||||
{ title: '原因', key: 'lastReason', align: 'left', width: 180 },
|
||||
{ title: '创建时间', key: 'createdAt', align: 'left', width: 180 },
|
||||
{ title: '更新时间', key: 'updatedAt', align: 'left', width: 180 },
|
||||
];
|
||||
|
||||
export function loadOptions() {
|
||||
dict.loadOptions(['ImKafkaDeadLetterStatusOptions']);
|
||||
}
|
||||
|
||||
export async function loadTenantOptions() {
|
||||
const res = await TenantList({ page: 1, perPage: 200 });
|
||||
tenantOptions.value = (res?.list || []).map((item) => ({
|
||||
label: `${item.name}(${item.code})`,
|
||||
value: item.id,
|
||||
}));
|
||||
return tenantOptions.value;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
:mask-closable="false"
|
||||
:show-icon="false"
|
||||
preset="dialog"
|
||||
transform-origin="center"
|
||||
:title="'死信详情 #' + formValue.id"
|
||||
:style="{ width: dialogWidth }"
|
||||
>
|
||||
<n-scrollbar style="max-height: 87vh" class="pr-5">
|
||||
<n-spin :show="loading" description="请稍候...">
|
||||
<n-descriptions label-placement="left" bordered :column="settingStore.isMobile ? 1 : 2">
|
||||
<n-descriptions-item label="死信ID">{{ formValue.deadId }}</n-descriptions-item>
|
||||
<n-descriptions-item label="状态">{{ statusLabel }}</n-descriptions-item>
|
||||
<n-descriptions-item label="事件ID">{{ formValue.eventId }}</n-descriptions-item>
|
||||
<n-descriptions-item label="父事件ID">{{ formValue.parentEventId || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="来源Topic">{{ formValue.sourceTopic || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="消费组">{{ formValue.consumerGroup || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="事件类型">{{ formValue.eventType || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="业务对象">{{ formValue.bizType }} / {{ formValue.bizId }}</n-descriptions-item>
|
||||
<n-descriptions-item label="重试次数">{{ formValue.retryCount }}</n-descriptions-item>
|
||||
<n-descriptions-item label="重放次数">{{ formValue.replayCount }}</n-descriptions-item>
|
||||
<n-descriptions-item label="最后原因">{{ formValue.lastReason || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="最后错误" :span="2">{{ formValue.lastError || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="Envelope" :span="2">
|
||||
<n-code :code="formValue.envelope || formValue.envelopeRaw || '-'" language="json" word-wrap />
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
<n-form class="mt-4" label-placement="left" :label-width="110">
|
||||
<n-form-item label="操作IM用户ID">
|
||||
<n-input-number v-model:value="actionForm.actorUserId" clearable class="w-full" />
|
||||
</n-form-item>
|
||||
<n-form-item label="处置备注">
|
||||
<n-input v-model:value="actionForm.note" type="textarea" clearable placeholder="请输入备注" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-spin>
|
||||
</n-scrollbar>
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button @click="closeForm">关闭</n-button>
|
||||
<n-button type="default" :loading="submitting" @click="submitAction('ignore')">忽略</n-button>
|
||||
<n-button type="default" :loading="submitting" @click="submitAction('resolve')">标记已处理</n-button>
|
||||
<n-button type="primary" :loading="submitting" @click="submitAction('replay')">重放</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { Ignore, Replay, Resolve, View } from '@/api/imDeadLetter';
|
||||
import { newState, State } from './model';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
import { adaModalWidth } from '@/utils/hotgo';
|
||||
import { useMessage } from 'naive-ui';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
const settingStore = useProjectSettingStore();
|
||||
const dict = useDictStore();
|
||||
const message = useMessage();
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const showModal = ref(false);
|
||||
const formValue = ref<State>(newState(null));
|
||||
const tenantId = ref(0);
|
||||
const actionForm = reactive({
|
||||
actorUserId: null as number | null,
|
||||
note: '',
|
||||
});
|
||||
const dialogWidth = computed(() => adaModalWidth(980));
|
||||
const statusLabel = computed(() => dict.getLabel('ImKafkaDeadLetterStatusOptions', formValue.value.status) || '-');
|
||||
|
||||
function closeForm() {
|
||||
showModal.value = false;
|
||||
loading.value = false;
|
||||
submitting.value = false;
|
||||
}
|
||||
|
||||
function openModal(state: State, currentTenantId: number) {
|
||||
tenantId.value = currentTenantId;
|
||||
showModal.value = true;
|
||||
loading.value = true;
|
||||
actionForm.actorUserId = null;
|
||||
actionForm.note = '';
|
||||
View({ tenantId: currentTenantId, deadId: state.deadId })
|
||||
.then((res) => {
|
||||
formValue.value = newState(res);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function submitAction(type: 'replay' | 'ignore' | 'resolve') {
|
||||
const params = {
|
||||
tenantId: tenantId.value,
|
||||
deadId: formValue.value.deadId,
|
||||
actorUserId: actionForm.actorUserId,
|
||||
note: actionForm.note,
|
||||
};
|
||||
submitting.value = true;
|
||||
try {
|
||||
const api = type === 'replay' ? Replay : type === 'ignore' ? Ignore : Resolve;
|
||||
const res = await api(params);
|
||||
message.success(`命令已提交:${res?.commandId || '-'}`);
|
||||
emit('reload');
|
||||
closeForm();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openModal,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="n-layout-page-header">
|
||||
<n-card :bordered="false" title="IM Kafka审计事件"></n-card>
|
||||
</div>
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<BasicForm
|
||||
ref="searchFormRef"
|
||||
@register="register"
|
||||
@submit="reloadTable"
|
||||
@reset="reloadTable"
|
||||
@keyup.enter="reloadTable"
|
||||
/>
|
||||
<BasicTable
|
||||
ref="actionRef"
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
:row-key="(row) => row.id"
|
||||
:actionColumn="actionColumn"
|
||||
:scroll-x="scrollX"
|
||||
:resizeHeightOffset="-10000"
|
||||
/>
|
||||
</n-card>
|
||||
<ViewModal ref="viewRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, reactive, ref } from 'vue';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { BasicForm, useForm } from '@/components/Form/index';
|
||||
import { List } from '@/api/imKafkaAudit';
|
||||
import { columns, schemas, State } from './model';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import ViewModal from './view.vue';
|
||||
|
||||
const actionRef = ref();
|
||||
const searchFormRef = ref<any>({});
|
||||
const viewRef = ref();
|
||||
|
||||
const actionColumn = reactive({
|
||||
width: 100,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render(record: State) {
|
||||
return h(TableAction as any, {
|
||||
style: 'button',
|
||||
actions: [{ label: '详情', onClick: () => viewRef.value.openModal(record), auth: ['/imKafkaAudit/view'] }],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
|
||||
|
||||
const [register] = useForm({
|
||||
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||
labelWidth: 90,
|
||||
schemas,
|
||||
});
|
||||
|
||||
const loadDataTable = async (res) => {
|
||||
const params = { ...searchFormRef.value?.formModel, ...res };
|
||||
return await List(params);
|
||||
};
|
||||
|
||||
function reloadTable() {
|
||||
actionRef.value?.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,106 @@
|
||||
import { ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { FormSchema } from '@/components/Form';
|
||||
import { defRangeShortcuts } from '@/utils/dateUtil';
|
||||
|
||||
export class State {
|
||||
public id = 0;
|
||||
public eventId = '';
|
||||
public tenantCode = '';
|
||||
public eventType = '';
|
||||
public bizType = '';
|
||||
public bizId = '';
|
||||
public actorType = '';
|
||||
public actorId = '';
|
||||
public action = '';
|
||||
public resource = '';
|
||||
public resourceId = '';
|
||||
public result = '';
|
||||
public reason = '';
|
||||
public payload = '';
|
||||
public envelope = '';
|
||||
public occurredAt = '';
|
||||
public receivedAt = '';
|
||||
|
||||
constructor(state?: Partial<State>) {
|
||||
if (state) Object.assign(this, state);
|
||||
}
|
||||
}
|
||||
|
||||
export function newState(state: State | Record<string, any> | null): State {
|
||||
if (state !== null) {
|
||||
if (state instanceof State) return cloneDeep(state);
|
||||
return new State(state);
|
||||
}
|
||||
return new State();
|
||||
}
|
||||
|
||||
export const schemas = ref<FormSchema[]>([
|
||||
{
|
||||
field: 'tenantCode',
|
||||
component: 'NInput',
|
||||
label: '租户编码',
|
||||
componentProps: { placeholder: '请输入租户编码' },
|
||||
},
|
||||
{
|
||||
field: 'eventId',
|
||||
component: 'NInput',
|
||||
label: '事件ID',
|
||||
componentProps: { placeholder: '请输入事件ID' },
|
||||
},
|
||||
{
|
||||
field: 'eventType',
|
||||
component: 'NInput',
|
||||
label: '事件类型',
|
||||
componentProps: { placeholder: '请输入事件类型' },
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
component: 'NInput',
|
||||
label: '动作',
|
||||
componentProps: { placeholder: '请输入动作' },
|
||||
},
|
||||
{
|
||||
field: 'resource',
|
||||
component: 'NInput',
|
||||
label: '资源',
|
||||
componentProps: { placeholder: '请输入资源' },
|
||||
},
|
||||
{
|
||||
field: 'resourceId',
|
||||
component: 'NInput',
|
||||
label: '资源ID',
|
||||
componentProps: { placeholder: '请输入资源ID' },
|
||||
},
|
||||
{
|
||||
field: 'result',
|
||||
component: 'NInput',
|
||||
label: '结果',
|
||||
componentProps: { placeholder: '请输入结果' },
|
||||
},
|
||||
{
|
||||
field: 'createdAt',
|
||||
component: 'NDatePicker',
|
||||
label: '接收时间',
|
||||
componentProps: {
|
||||
type: 'datetimerange',
|
||||
clearable: true,
|
||||
shortcuts: defRangeShortcuts(),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
export const columns = [
|
||||
{ title: 'ID', key: 'id', align: 'left', width: 90 },
|
||||
{ title: '租户', key: 'tenantCode', align: 'left', width: 120 },
|
||||
{ title: '事件ID', key: 'eventId', align: 'left', width: 220 },
|
||||
{ title: '事件类型', key: 'eventType', align: 'left', width: 190 },
|
||||
{ title: '动作', key: 'action', align: 'left', width: 170 },
|
||||
{ title: '资源', key: 'resource', align: 'left', width: 120 },
|
||||
{ title: '资源ID', key: 'resourceId', align: 'left', width: 170 },
|
||||
{ title: '操作者', key: 'actorId', align: 'left', width: 140 },
|
||||
{ title: '结果', key: 'result', align: 'left', width: 100 },
|
||||
{ title: '原因', key: 'reason', align: 'left', width: 160 },
|
||||
{ title: '发生时间', key: 'occurredAt', align: 'left', width: 180 },
|
||||
{ title: '接收时间', key: 'receivedAt', align: 'left', width: 180 },
|
||||
];
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
:mask-closable="false"
|
||||
:show-icon="false"
|
||||
preset="dialog"
|
||||
transform-origin="center"
|
||||
:title="'Kafka审计事件 #' + formValue.id"
|
||||
:style="{ width: dialogWidth }"
|
||||
>
|
||||
<n-scrollbar style="max-height: 87vh" class="pr-5">
|
||||
<n-spin :show="loading" description="请稍候...">
|
||||
<n-descriptions label-placement="left" bordered :column="settingStore.isMobile ? 1 : 2">
|
||||
<n-descriptions-item label="租户">{{ formValue.tenantCode || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="事件类型">{{ formValue.eventType || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="事件ID">{{ formValue.eventId || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="业务ID">{{ formValue.bizId || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="操作者">{{ formValue.actorType || '-' }} / {{ formValue.actorId || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="动作">{{ formValue.action || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="资源">{{ formValue.resource || '-' }} / {{ formValue.resourceId || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="结果">{{ formValue.result || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="原因" :span="2">{{ formValue.reason || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="发生时间">{{ formValue.occurredAt || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="接收时间">{{ formValue.receivedAt || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="Payload" :span="2">
|
||||
<n-code :code="formValue.payload || '-'" language="json" word-wrap />
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="Envelope" :span="2">
|
||||
<n-code :code="formValue.envelope || '-'" language="json" word-wrap />
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-spin>
|
||||
</n-scrollbar>
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button @click="closeForm">关闭</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { View } from '@/api/imKafkaAudit';
|
||||
import { newState, State } from './model';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
import { adaModalWidth } from '@/utils/hotgo';
|
||||
|
||||
const settingStore = useProjectSettingStore();
|
||||
const loading = ref(false);
|
||||
const showModal = ref(false);
|
||||
const formValue = ref<State>(newState(null));
|
||||
const dialogWidth = computed(() => adaModalWidth(980));
|
||||
|
||||
function closeForm() {
|
||||
showModal.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function openModal(state: State) {
|
||||
showModal.value = true;
|
||||
loading.value = true;
|
||||
View({ id: state.id })
|
||||
.then((res) => {
|
||||
formValue.value = newState(res);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openModal,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="n-layout-page-header">
|
||||
<n-card :bordered="false" title="IM后台命令"></n-card>
|
||||
</div>
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<BasicForm
|
||||
ref="searchFormRef"
|
||||
@register="register"
|
||||
@submit="reloadTable"
|
||||
@reset="reloadTable"
|
||||
@keyup.enter="reloadTable"
|
||||
/>
|
||||
<n-space class="table-toolbar">
|
||||
<n-button type="primary" @click="sendCacheInvalidate">发送缓存失效测试</n-button>
|
||||
<n-button @click="sendWorkbenchEvent">发送工作台测试事件</n-button>
|
||||
</n-space>
|
||||
<BasicTable
|
||||
ref="actionRef"
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
:row-key="(row) => row.id"
|
||||
:actionColumn="actionColumn"
|
||||
:scroll-x="scrollX"
|
||||
:resizeHeightOffset="-10000"
|
||||
/>
|
||||
</n-card>
|
||||
<ViewModal ref="viewRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { BasicForm, useForm } from '@/components/Form/index';
|
||||
import { CacheInvalidate, List, WorkbenchEvent } from '@/api/imKafkaCommand';
|
||||
import { columns, loadOptions, loadTenantOptions, schemas, State, tenantOptions } from './model';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import ViewModal from './view.vue';
|
||||
|
||||
const actionRef = ref();
|
||||
const searchFormRef = ref<any>({});
|
||||
const viewRef = ref();
|
||||
const message = useMessage();
|
||||
|
||||
const actionColumn = reactive({
|
||||
width: 100,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render(record: State) {
|
||||
return h(TableAction as any, {
|
||||
style: 'button',
|
||||
actions: [{ label: '详情', onClick: () => viewRef.value.openModal(record), auth: ['/imKafkaCommand/view'] }],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
|
||||
|
||||
const [register] = useForm({
|
||||
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||
labelWidth: 90,
|
||||
schemas,
|
||||
});
|
||||
|
||||
const loadDataTable = async (res) => {
|
||||
const params = { ...searchFormRef.value?.formModel, ...res };
|
||||
return await List(params);
|
||||
};
|
||||
|
||||
function reloadTable() {
|
||||
actionRef.value?.reload();
|
||||
}
|
||||
|
||||
async function sendCacheInvalidate() {
|
||||
const tenant = getFirstTenant();
|
||||
if (!tenant) {
|
||||
message.warning('暂无可用租户');
|
||||
return;
|
||||
}
|
||||
const res = await CacheInvalidate({
|
||||
tenantCode: tenant.code,
|
||||
namespace: 'kafka_connectivity',
|
||||
keys: ['healthcheck'],
|
||||
reason: 'admin_connectivity_test',
|
||||
});
|
||||
message.success(`已发送缓存失效测试事件:${res?.eventId || ''}`);
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
async function sendWorkbenchEvent() {
|
||||
const tenant = getFirstTenant();
|
||||
if (!tenant) {
|
||||
message.warning('暂无可用租户');
|
||||
return;
|
||||
}
|
||||
const res = await WorkbenchEvent({
|
||||
tenantCode: tenant.code,
|
||||
eventType: 'workbench.connectivity.test',
|
||||
bizId: 'admin_connectivity_test',
|
||||
payload: { message: 'admin workbench event connectivity test' },
|
||||
});
|
||||
message.success(`已发送工作台测试事件:${res?.eventId || ''}`);
|
||||
}
|
||||
|
||||
function getFirstTenant() {
|
||||
const tenant = tenantOptions.value[0];
|
||||
if (!tenant) return null;
|
||||
return {
|
||||
...tenant,
|
||||
code: tenant.code || String(tenant.label || '').match(/((.+))/)?.[1] || 'default',
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadOptions();
|
||||
await loadTenantOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.table-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
import { computed, ref } from 'vue';
|
||||
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';
|
||||
|
||||
const dict = useDictStore();
|
||||
|
||||
export const tenantOptions = ref<any[]>([]);
|
||||
|
||||
export class State {
|
||||
public id = 0;
|
||||
public commandId = '';
|
||||
public tenantId = 0;
|
||||
public tenantCode = '';
|
||||
public eventId = '';
|
||||
public eventType = '';
|
||||
public bizType = '';
|
||||
public bizId = '';
|
||||
public idempotencyKey = '';
|
||||
public status = '';
|
||||
public reason = '';
|
||||
public message = '';
|
||||
public payload = '';
|
||||
public envelope = '';
|
||||
public result = '';
|
||||
public sentAt = '';
|
||||
public completedAt = '';
|
||||
public createdBy = 0;
|
||||
public createdAt = '';
|
||||
public updatedAt = '';
|
||||
|
||||
constructor(state?: Partial<State>) {
|
||||
if (state) Object.assign(this, state);
|
||||
}
|
||||
}
|
||||
|
||||
export function newState(state: State | Record<string, any> | null): State {
|
||||
if (state !== null) {
|
||||
if (state instanceof State) return cloneDeep(state);
|
||||
return new State(state);
|
||||
}
|
||||
return new State();
|
||||
}
|
||||
|
||||
export const schemas = ref<FormSchema[]>([
|
||||
{
|
||||
field: 'tenantId',
|
||||
component: 'NSelect',
|
||||
label: '租户',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
options: computed(() => tenantOptions.value),
|
||||
placeholder: '请选择租户',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'commandId',
|
||||
component: 'NInput',
|
||||
label: '命令ID',
|
||||
componentProps: { placeholder: '请输入命令ID' },
|
||||
},
|
||||
{
|
||||
field: 'eventId',
|
||||
component: 'NInput',
|
||||
label: '事件ID',
|
||||
componentProps: { placeholder: '请输入事件ID' },
|
||||
},
|
||||
{
|
||||
field: 'eventType',
|
||||
component: 'NInput',
|
||||
label: '事件类型',
|
||||
componentProps: { placeholder: '请输入事件类型' },
|
||||
},
|
||||
{
|
||||
field: 'bizId',
|
||||
component: 'NInput',
|
||||
label: '业务ID',
|
||||
componentProps: { placeholder: '请输入业务ID' },
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
component: 'NSelect',
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
options: computed(() => dict.getOptions('ImAdminCommandStatusOptions')),
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createdAt',
|
||||
component: 'NDatePicker',
|
||||
label: '创建时间',
|
||||
componentProps: {
|
||||
type: 'datetimerange',
|
||||
clearable: true,
|
||||
shortcuts: defRangeShortcuts(),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
export const columns = [
|
||||
{ title: 'ID', key: 'id', align: 'left', width: 90 },
|
||||
{ title: '租户', key: 'tenantCode', align: 'left', width: 120 },
|
||||
{ title: '命令ID', key: 'commandId', align: 'left', width: 210 },
|
||||
{ title: '事件类型', key: 'eventType', align: 'left', width: 190 },
|
||||
{ title: '业务类型', key: 'bizType', align: 'left', width: 110 },
|
||||
{ title: '业务ID', key: 'bizId', align: 'left', width: 180 },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
align: 'left',
|
||||
width: 110,
|
||||
render(row: State) {
|
||||
return dict.getLabel('ImAdminCommandStatusOptions', row.status);
|
||||
},
|
||||
},
|
||||
{ title: '错误码', key: 'reason', align: 'left', width: 170 },
|
||||
{ title: '消息', key: 'message', align: 'left', width: 220 },
|
||||
{ title: '投递时间', key: 'sentAt', align: 'left', width: 180 },
|
||||
{ title: '完成时间', key: 'completedAt', align: 'left', width: 180 },
|
||||
{ title: '创建时间', key: 'createdAt', align: 'left', width: 180 },
|
||||
];
|
||||
|
||||
export function loadOptions() {
|
||||
dict.loadOptions(['ImAdminCommandStatusOptions']);
|
||||
}
|
||||
|
||||
export async function loadTenantOptions() {
|
||||
const res = await TenantList({ page: 1, perPage: 20 });
|
||||
tenantOptions.value = (res?.list || []).map((item) => ({
|
||||
label: `${item.name}(${item.code})`,
|
||||
value: item.id,
|
||||
code: item.code,
|
||||
}));
|
||||
return tenantOptions.value;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
:mask-closable="false"
|
||||
:show-icon="false"
|
||||
preset="dialog"
|
||||
transform-origin="center"
|
||||
:title="'命令详情 #' + formValue.id"
|
||||
:style="{ width: dialogWidth }"
|
||||
>
|
||||
<n-scrollbar style="max-height: 87vh" class="pr-5">
|
||||
<n-spin :show="loading" description="请稍候...">
|
||||
<n-descriptions label-placement="left" bordered :column="settingStore.isMobile ? 1 : 2">
|
||||
<n-descriptions-item label="租户">{{ formValue.tenantCode || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="状态">{{ statusLabel }}</n-descriptions-item>
|
||||
<n-descriptions-item label="命令ID">{{ formValue.commandId }}</n-descriptions-item>
|
||||
<n-descriptions-item label="事件ID">{{ formValue.eventId }}</n-descriptions-item>
|
||||
<n-descriptions-item label="事件类型">{{ formValue.eventType }}</n-descriptions-item>
|
||||
<n-descriptions-item label="业务对象">{{ formValue.bizType }} / {{ formValue.bizId }}</n-descriptions-item>
|
||||
<n-descriptions-item label="幂等键">{{ formValue.idempotencyKey || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="创建人">{{ formValue.createdBy || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="投递时间">{{ formValue.sentAt || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="完成时间">{{ formValue.completedAt || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="错误码">{{ formValue.reason || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="消息">{{ formValue.message || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="Payload" :span="2">
|
||||
<n-code :code="formValue.payload || '-'" language="json" word-wrap />
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="Envelope" :span="2">
|
||||
<n-code :code="formValue.envelope || '-'" language="json" word-wrap />
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="Result" :span="2">
|
||||
<n-code :code="formValue.result || '-'" language="json" word-wrap />
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-spin>
|
||||
</n-scrollbar>
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button @click="closeForm">关闭</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { View } from '@/api/imKafkaCommand';
|
||||
import { newState, State } from './model';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
import { adaModalWidth } from '@/utils/hotgo';
|
||||
|
||||
const settingStore = useProjectSettingStore();
|
||||
const dict = useDictStore();
|
||||
const loading = ref(false);
|
||||
const showModal = ref(false);
|
||||
const formValue = ref<State>(newState(null));
|
||||
const dialogWidth = computed(() => adaModalWidth(980));
|
||||
const statusLabel = computed(() => dict.getLabel('ImAdminCommandStatusOptions', formValue.value.status) || '-');
|
||||
|
||||
function closeForm() {
|
||||
showModal.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function openModal(state: State) {
|
||||
showModal.value = true;
|
||||
loading.value = true;
|
||||
View({ id: state.id })
|
||||
.then((res) => {
|
||||
formValue.value = newState(res);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openModal,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
:mask-closable="false"
|
||||
:show-icon="false"
|
||||
preset="dialog"
|
||||
transform-origin="center"
|
||||
:title="'用户治理 #' + formValue.userId"
|
||||
:style="{ width: dialogWidth }"
|
||||
>
|
||||
<n-spin :show="loading" description="请稍候...">
|
||||
<n-descriptions label-placement="left" bordered :column="settingStore.isMobile ? 1 : 2">
|
||||
<n-descriptions-item label="租户">{{ formValue.tenantName || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="用户ID">{{ formValue.userId }}</n-descriptions-item>
|
||||
<n-descriptions-item label="昵称">{{ formValue.nickname || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="状态">{{ stateLabel }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
<n-form class="mt-4" label-placement="left" :label-width="110">
|
||||
<n-form-item label="治理动作">
|
||||
<n-select v-model:value="actionForm.action" :options="actionOptions" />
|
||||
</n-form-item>
|
||||
<n-form-item label="操作IM用户ID">
|
||||
<n-input-number v-model:value="actionForm.actorUserId" clearable class="w-full" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="actionForm.action === 'deviceKick'" label="设备ID">
|
||||
<n-input v-model:value="actionForm.deviceId" clearable placeholder="请输入 device_id" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="actionForm.action === 'accountCancel'" label="硬删除">
|
||||
<n-switch v-model:value="actionForm.hard" />
|
||||
</n-form-item>
|
||||
<n-form-item label="原因">
|
||||
<n-input v-model:value="actionForm.reason" type="textarea" clearable placeholder="请输入操作原因" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-spin>
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button @click="closeForm">关闭</n-button>
|
||||
<n-button type="primary" :loading="loading" @click="submitAction">提交命令</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import {
|
||||
AccountCancel,
|
||||
AccountDelete,
|
||||
Ban,
|
||||
DeviceKick,
|
||||
Freeze,
|
||||
LogoutAll,
|
||||
Unban,
|
||||
Unfreeze,
|
||||
} from '@/api/imUser';
|
||||
import { newState, State } from './model';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
import { adaModalWidth } from '@/utils/hotgo';
|
||||
import { useMessage } from 'naive-ui';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
const settingStore = useProjectSettingStore();
|
||||
const dict = useDictStore();
|
||||
const message = useMessage();
|
||||
const loading = ref(false);
|
||||
const showModal = ref(false);
|
||||
const formValue = ref<State>(newState(null));
|
||||
const dialogWidth = computed(() => adaModalWidth(720));
|
||||
const stateLabel = computed(() => dict.getLabel('ImUserStateOptions', formValue.value.state) || '-');
|
||||
|
||||
const actionOptions = [
|
||||
{ label: '封禁用户', value: 'ban' },
|
||||
{ label: '解封用户', value: 'unban' },
|
||||
{ label: '冻结用户', value: 'freeze' },
|
||||
{ label: '解冻用户', value: 'unfreeze' },
|
||||
{ label: '踢出指定设备', value: 'deviceKick' },
|
||||
{ label: '踢出全部会话', value: 'logoutAll' },
|
||||
{ label: '注销账号', value: 'accountCancel' },
|
||||
{ label: '删除账号', value: 'accountDelete' },
|
||||
];
|
||||
|
||||
const actionForm = reactive({
|
||||
action: 'ban',
|
||||
actorUserId: null as number | null,
|
||||
deviceId: '',
|
||||
reason: '',
|
||||
hard: false,
|
||||
});
|
||||
|
||||
function closeForm() {
|
||||
showModal.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function openModal(state: State) {
|
||||
formValue.value = newState(state);
|
||||
actionForm.action = 'ban';
|
||||
actionForm.actorUserId = state.userId || null;
|
||||
actionForm.deviceId = '';
|
||||
actionForm.reason = '';
|
||||
actionForm.hard = false;
|
||||
showModal.value = true;
|
||||
}
|
||||
|
||||
async function submitAction() {
|
||||
const params = {
|
||||
tenantId: formValue.value.tenantId,
|
||||
userId: formValue.value.userId,
|
||||
actorUserId: actionForm.actorUserId,
|
||||
deviceId: actionForm.deviceId,
|
||||
reason: actionForm.reason,
|
||||
hard: actionForm.hard,
|
||||
};
|
||||
const apiMap = {
|
||||
ban: Ban,
|
||||
unban: Unban,
|
||||
freeze: Freeze,
|
||||
unfreeze: Unfreeze,
|
||||
deviceKick: DeviceKick,
|
||||
logoutAll: LogoutAll,
|
||||
accountCancel: AccountCancel,
|
||||
accountDelete: AccountDelete,
|
||||
};
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await apiMap[actionForm.action](params);
|
||||
message.success(`命令已提交:${res?.commandId || '-'}`);
|
||||
emit('reload');
|
||||
closeForm();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openModal,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
Reference in New Issue
Block a user