first commit
This commit is contained in:
78
src/views/monitor/log/index.vue
Normal file
78
src/views/monitor/log/index.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="gi_table_page">
|
||||
<!-- <a-row justify="space-between" align="center" class="header">
|
||||
<a-space wrap>
|
||||
<div class="title">系统日志</div>
|
||||
</a-space>
|
||||
</a-row> -->
|
||||
<a-tabs v-model:active-key="activeKey" type="card-gutter" size="large" @change="change">
|
||||
<a-tab-pane key="1">
|
||||
<template #title><icon-lock /> 登录日志</template>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2">
|
||||
<template #title><icon-find-replace /> 操作日志</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<keep-alive>
|
||||
<component :is="PaneMap[activeKey]" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LoginLog from './login/index.vue'
|
||||
import OperationLog from './operation/index.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const PaneMap: Record<string, Component> = {
|
||||
1: LoginLog,
|
||||
2: OperationLog,
|
||||
}
|
||||
|
||||
const activeKey = ref('1')
|
||||
watch(
|
||||
() => route.query,
|
||||
() => {
|
||||
if (route.query.tabKey) {
|
||||
activeKey.value = String(route.query.tabKey)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const change = (key: string | number) => {
|
||||
activeKey.value = key as string
|
||||
router.replace({ path: route.path, query: { tabKey: key } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.arco-tabs .arco-tabs-nav-type-card-gutter .arco-tabs-tab-active) {
|
||||
box-shadow: inset 0 2px 0 rgb(var(--primary-6)), inset -1px 0 0 var(--color-border-2),
|
||||
inset 1px 0 0 var(--color-border-2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav-type-card-gutter .arco-tabs-tab) {
|
||||
border-radius: var(--border-radius-medium) var(--border-radius-medium) 0 0;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-type-card-gutter > .arco-tabs-content) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav::before) {
|
||||
left: -20px;
|
||||
right: -20px;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav) {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
138
src/views/monitor/log/login/index.vue
Normal file
138
src/views/monitor/log/login/index.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<GiTable
|
||||
row-key="id"
|
||||
:data="dataList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
|
||||
:pagination="pagination"
|
||||
:disabled-tools="['size', 'setting']"
|
||||
@filter-change="filterChange"
|
||||
@refresh="search"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-input v-model="queryForm.createUserString" placeholder="搜索登录用户" allow-clear @change="search">
|
||||
<template #prefix><icon-search /></template>
|
||||
</a-input>
|
||||
<a-input v-model="queryForm.ip" placeholder="搜索登录 IP 或地点" allow-clear @change="search">
|
||||
<template #prefix><icon-search /></template>
|
||||
</a-input>
|
||||
<DateRangePicker v-model="queryForm.createTime" @change="search" />
|
||||
<a-button @click="reset">
|
||||
<template #icon><icon-refresh /></template>
|
||||
<template #default>重置</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<template #toolbar-right>
|
||||
<a-button v-permission="['monitor:log:export']" @click="onExport">
|
||||
<template #icon><icon-download /></template>
|
||||
<template #default>导出</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag v-if="record.status === 1" color="green">
|
||||
<GiDot type="success" style="width: 5px; height: 5px" />
|
||||
<span style="margin-left: 5px">成功</span>
|
||||
</a-tag>
|
||||
<a-tooltip v-else :content="record.errorMsg">
|
||||
<a-tag color="red" style="cursor: pointer">
|
||||
<GiDot type="danger" style="width: 5px; height: 5px" />
|
||||
<span style="margin-left: 5px">失败</span>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</GiTable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import { type LogQuery, exportLoginLog, listLog } from '@/apis/monitor'
|
||||
import type { TableInstanceColumns } from '@/components/GiTable/type'
|
||||
import DateRangePicker from '@/components/DateRangePicker/index.vue'
|
||||
import { useDownload, useTable } from '@/hooks'
|
||||
|
||||
defineOptions({ name: 'LoginLog' })
|
||||
|
||||
const queryForm = reactive<LogQuery>({
|
||||
module: '登录',
|
||||
createTime: [
|
||||
dayjs().subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||||
dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||||
],
|
||||
sort: ['createTime,desc'],
|
||||
})
|
||||
|
||||
const {
|
||||
tableData: dataList,
|
||||
loading,
|
||||
pagination,
|
||||
search,
|
||||
} = useTable((page) => listLog({ ...queryForm, ...page }), { immediate: true })
|
||||
|
||||
const columns: TableInstanceColumns[] = [
|
||||
{
|
||||
title: '序号',
|
||||
width: 66,
|
||||
align: 'center',
|
||||
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
|
||||
},
|
||||
{ title: '登录时间', dataIndex: 'createTime', width: 180 },
|
||||
{ title: '用户昵称', dataIndex: 'createUserString', ellipsis: true, tooltip: true },
|
||||
{ title: '登录行为', dataIndex: 'description' },
|
||||
{
|
||||
title: '状态',
|
||||
slotName: 'status',
|
||||
align: 'center',
|
||||
filterable: {
|
||||
filters: [
|
||||
{
|
||||
text: '成功',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
text: '失败',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
filter: () => {
|
||||
return true
|
||||
},
|
||||
alignLeft: true,
|
||||
},
|
||||
},
|
||||
{ title: '登录 IP', dataIndex: 'ip', ellipsis: true, tooltip: true },
|
||||
{ title: '登录地点', dataIndex: 'address', ellipsis: true, tooltip: true },
|
||||
{ title: '浏览器', dataIndex: 'browser', ellipsis: true, tooltip: true },
|
||||
{ title: '终端系统', dataIndex: 'os', ellipsis: true, tooltip: true },
|
||||
]
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
queryForm.ip = undefined
|
||||
queryForm.createUserString = undefined
|
||||
queryForm.createTime = [
|
||||
dayjs().subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||||
dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||||
]
|
||||
queryForm.status = undefined
|
||||
search()
|
||||
}
|
||||
|
||||
// 过滤查询
|
||||
const filterChange = (dataIndex, filteredValues) => {
|
||||
try {
|
||||
const slotName = columns[dataIndex.split('_').pop()].slotName as string
|
||||
queryForm[slotName] = filteredValues.join(',')
|
||||
search()
|
||||
} catch (error) {
|
||||
search()
|
||||
}
|
||||
}
|
||||
|
||||
// 导出
|
||||
const onExport = () => {
|
||||
useDownload(() => exportLoginLog(queryForm))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
102
src/views/monitor/log/operation/OperationLogDetailDrawer.vue
Normal file
102
src/views/monitor/log/operation/OperationLogDetailDrawer.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible" title="日志详情" :width="720" :footer="false">
|
||||
<a-descriptions title="基本信息" :column="2" size="large" class="general-description">
|
||||
<a-descriptions-item label="日志 ID">{{ dataDetail?.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="Trace ID"><a-typography-paragraph :copyable="!!dataDetail?.traceId">{{ dataDetail?.traceId }}</a-typography-paragraph></a-descriptions-item>
|
||||
<a-descriptions-item label="操作人">{{ dataDetail?.createUserString }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作时间">{{ dataDetail?.createTime }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作内容">{{ dataDetail?.description }}</a-descriptions-item>
|
||||
<a-descriptions-item label="所属模块">{{ dataDetail?.module }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作 IP"><a-typography-paragraph :copyable="!!dataDetail?.ip">{{ dataDetail?.ip }}</a-typography-paragraph></a-descriptions-item>
|
||||
<a-descriptions-item label="操作地点">{{ dataDetail?.address }}</a-descriptions-item>
|
||||
<a-descriptions-item label="浏览器">{{ dataDetail?.browser }}</a-descriptions-item>
|
||||
<a-descriptions-item label="终端系统">{{ dataDetail?.os }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag v-if="dataDetail?.status === 1" color="green">成功</a-tag>
|
||||
<a-tag v-else color="red">失败</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="耗时">
|
||||
<a-tag v-if="dataDetail?.timeTaken > 500" color="red"> {{ dataDetail?.timeTaken }}ms </a-tag>
|
||||
<a-tag v-else-if="dataDetail?.timeTaken > 200" color="orange"> {{ dataDetail?.timeTaken }}ms </a-tag>
|
||||
<a-tag v-else color="green">{{ dataDetail?.timeTaken }} ms</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="请求 URI" :span="2">
|
||||
<a-typography-paragraph :copyable="!!dataDetail?.requestUrl">{{ dataDetail?.requestUrl }}</a-typography-paragraph>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-descriptions
|
||||
title="响应信息"
|
||||
:column="2"
|
||||
size="large"
|
||||
class="general-description http"
|
||||
style="margin-top: 20px; position: relative"
|
||||
>
|
||||
<a-descriptions-item :span="2">
|
||||
<a-tabs type="card">
|
||||
<a-tab-pane key="1" title="响应头">
|
||||
<JsonPretty v-if="dataDetail?.responseHeaders" :json="dataDetail?.responseHeaders" />
|
||||
<span v-else>无</span>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" title="响应体">
|
||||
<JsonPretty v-if="dataDetail?.responseBody" :json="dataDetail?.responseBody" />
|
||||
<span v-else>无</span>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-descriptions
|
||||
title="请求信息"
|
||||
:column="2"
|
||||
size="large"
|
||||
class="general-description http"
|
||||
style="margin-top: 20px; position: relative"
|
||||
>
|
||||
<a-descriptions-item :span="2">
|
||||
<a-tabs type="card">
|
||||
<a-tab-pane key="1" title="请求头">
|
||||
<JsonPretty v-if="dataDetail?.requestHeaders" :json="dataDetail?.requestHeaders" />
|
||||
<span v-else>无</span>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" title="请求体">
|
||||
<JsonPretty v-if="dataDetail?.requestBody" :json="dataDetail?.requestBody" />
|
||||
<span v-else>无</span>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type LogDetailResp, getLog as getDetail } from '@/apis/monitor'
|
||||
|
||||
const dataId = ref('')
|
||||
const dataDetail = ref<LogDetailResp>()
|
||||
const visible = ref(false)
|
||||
|
||||
// 查询详情
|
||||
const getDataDetail = async () => {
|
||||
const { data } = await getDetail(dataId.value)
|
||||
dataDetail.value = data
|
||||
}
|
||||
|
||||
// 打开
|
||||
const onOpen = async (id: string) => {
|
||||
dataId.value = id
|
||||
await getDataDetail()
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
defineExpose({ onOpen })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.http :deep(.arco-descriptions-item-label-block) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-content) {
|
||||
padding-top: 5px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
</style>
|
||||
157
src/views/monitor/log/operation/index.vue
Normal file
157
src/views/monitor/log/operation/index.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<GiTable
|
||||
row-key="id"
|
||||
:data="dataList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
|
||||
:pagination="pagination"
|
||||
column-resizable
|
||||
:disabled-tools="['size', 'setting']"
|
||||
@filter-change="filterChange"
|
||||
@refresh="search"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-input v-model="queryForm.createUserString" placeholder="搜索操作人" allow-clear @change="search">
|
||||
<template #prefix><icon-search /></template>
|
||||
</a-input>
|
||||
<a-input v-model="queryForm.ip" placeholder="搜索操作 IP 或地点" allow-clear @change="search">
|
||||
<template #prefix><icon-search /></template>
|
||||
</a-input>
|
||||
<DateRangePicker v-model="queryForm.createTime" @change="search" />
|
||||
<a-button @click="reset">
|
||||
<template #icon><icon-refresh /></template>
|
||||
<template #default>重置</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<template #toolbar-right>
|
||||
<a-button v-permission="['monitor:log:export']" @click="onExport">
|
||||
<template #icon><icon-download /></template>
|
||||
<template #default>导出</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-if="has.hasPermOr(['monitor:log:detail'])" #createTime="{ record }">
|
||||
<a-link @click="onDetail(record)">{{ record.createTime }}</a-link>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag v-if="record.status === 1" color="green">
|
||||
<GiDot type="success" style="width: 5px; height: 5px"></GiDot>
|
||||
<span style="margin-left: 5px">成功</span>
|
||||
</a-tag>
|
||||
<a-tooltip v-else :content="record.errorMsg">
|
||||
<a-tag color="red" style="cursor: pointer">
|
||||
<GiDot type="danger" style="width: 5px; height: 5px"></GiDot>
|
||||
<span style="margin-left: 5px">失败</span>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template #timeTaken="{ record }">
|
||||
<a-tag v-if="record.timeTaken > 500" color="red">{{ record.timeTaken }}ms</a-tag>
|
||||
<a-tag v-else-if="record.timeTaken > 200" color="orange">{{ record.timeTaken }}ms</a-tag>
|
||||
<a-tag v-else color="green">{{ record.timeTaken }} ms</a-tag>
|
||||
</template>
|
||||
</GiTable>
|
||||
|
||||
<OperationLogDetailDrawer ref="OperationLogDetailDrawerRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import OperationLogDetailDrawer from './OperationLogDetailDrawer.vue'
|
||||
import { type LogQuery, type LogResp, exportOperationLog, listLog } from '@/apis/monitor'
|
||||
import type { TableInstanceColumns } from '@/components/GiTable/type'
|
||||
import DateRangePicker from '@/components/DateRangePicker/index.vue'
|
||||
import { useDownload, useTable } from '@/hooks'
|
||||
import has from '@/utils/has'
|
||||
|
||||
defineOptions({ name: 'OperationLog' })
|
||||
|
||||
const queryForm = reactive<LogQuery>({
|
||||
createTime: [
|
||||
dayjs().subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||||
dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||||
],
|
||||
sort: ['createTime,desc'],
|
||||
})
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData: dataList,
|
||||
pagination,
|
||||
search,
|
||||
} = useTable((page) => listLog({ ...queryForm, ...page }), { immediate: true })
|
||||
const columns: TableInstanceColumns[] = [
|
||||
{
|
||||
title: '序号',
|
||||
width: 66,
|
||||
align: 'center',
|
||||
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
|
||||
},
|
||||
{ title: '操作时间', dataIndex: 'createTime', slotName: 'createTime', width: 180 },
|
||||
{ title: '操作人', dataIndex: 'createUserString', ellipsis: true, tooltip: true },
|
||||
{ title: '操作内容', dataIndex: 'description', ellipsis: true, tooltip: true },
|
||||
{ title: '所属模块', dataIndex: 'module', align: 'center', ellipsis: true, tooltip: true },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
align: 'center',
|
||||
filterable: {
|
||||
filters: [
|
||||
{
|
||||
text: '成功',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
text: '失败',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
filter: () => true,
|
||||
alignLeft: true,
|
||||
},
|
||||
},
|
||||
{ title: '操作 IP', dataIndex: 'ip', ellipsis: true, tooltip: true },
|
||||
{ title: '操作地点', dataIndex: 'address', ellipsis: true, tooltip: true },
|
||||
{ title: '耗时', dataIndex: 'timeTaken', slotName: 'timeTaken', align: 'center' },
|
||||
{ title: '浏览器', dataIndex: 'browser', ellipsis: true, tooltip: true },
|
||||
{ title: '终端系统', dataIndex: 'os', ellipsis: true, tooltip: true },
|
||||
]
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
queryForm.description = undefined
|
||||
queryForm.ip = undefined
|
||||
queryForm.createUserString = undefined
|
||||
queryForm.createTime = [
|
||||
dayjs().subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||||
dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||||
]
|
||||
queryForm.status = undefined
|
||||
search()
|
||||
}
|
||||
|
||||
// 过滤查询
|
||||
const filterChange = (dataIndex, filteredValues) => {
|
||||
try {
|
||||
const slotName = columns[dataIndex.split('_').pop()].slotName as string
|
||||
queryForm[slotName] = filteredValues.join(',')
|
||||
search()
|
||||
} catch (error) {
|
||||
search()
|
||||
}
|
||||
}
|
||||
|
||||
// 导出
|
||||
const onExport = () => {
|
||||
useDownload(() => exportOperationLog(queryForm))
|
||||
}
|
||||
|
||||
const OperationLogDetailDrawerRef = ref<InstanceType<typeof OperationLogDetailDrawer>>()
|
||||
// 详情
|
||||
const onDetail = (item: LogResp) => {
|
||||
OperationLogDetailDrawerRef.value?.onOpen(item.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
111
src/views/monitor/online/index.vue
Normal file
111
src/views/monitor/online/index.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="gi_table_page">
|
||||
<GiTable
|
||||
title=""
|
||||
row-key="id"
|
||||
:data="dataList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
|
||||
:pagination="pagination"
|
||||
:disabled-tools="['size']"
|
||||
@refresh="search"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-input-search v-model="queryForm.nickname" placeholder="搜索用户名/昵称" allow-clear @search="search" />
|
||||
<DateRangePicker v-model="queryForm.loginTime" @change="search" />
|
||||
<a-button @click="reset">
|
||||
<template #icon><icon-refresh /></template>
|
||||
<template #default>重置</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<template #nickname="{ record }">{{ record.nickname }}({{ record.username }})</template>
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-popconfirm
|
||||
type="warning"
|
||||
content="是否确定强退该用户?"
|
||||
:ok-button-props="{ status: 'danger' }"
|
||||
@ok="handleKickout(record.token)"
|
||||
>
|
||||
<a-link
|
||||
v-permission="['monitor:online:kickout']"
|
||||
status="danger"
|
||||
:title="currentToken === record.token ? '不能强退自己' : '强退'"
|
||||
:disabled="currentToken === record.token"
|
||||
>
|
||||
强退
|
||||
</a-link>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</GiTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { type OnlineUserQuery, kickout, listOnlineUser } from '@/apis/monitor'
|
||||
import type { TableInstanceColumns } from '@/components/GiTable/type'
|
||||
import DateRangePicker from '@/components/DateRangePicker/index.vue'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { useTable } from '@/hooks'
|
||||
import { isMobile } from '@/utils'
|
||||
import has from '@/utils/has'
|
||||
|
||||
defineOptions({ name: 'MonitorOnline' })
|
||||
|
||||
const userStore = useUserStore()
|
||||
const currentToken = userStore.token
|
||||
|
||||
const queryForm = reactive<OnlineUserQuery>({
|
||||
sort: ['createTime,desc'],
|
||||
})
|
||||
|
||||
const {
|
||||
tableData: dataList,
|
||||
loading,
|
||||
pagination,
|
||||
search,
|
||||
} = useTable((page) => listOnlineUser({ ...queryForm, ...page }), { immediate: true })
|
||||
const columns: TableInstanceColumns[] = [
|
||||
{
|
||||
title: '序号',
|
||||
width: 66,
|
||||
align: 'center',
|
||||
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
|
||||
},
|
||||
{ title: '用户昵称', dataIndex: 'nickname', slotName: 'nickname', ellipsis: true, tooltip: true },
|
||||
{ title: '登录 IP', dataIndex: 'ip', ellipsis: true, tooltip: true },
|
||||
{ title: '登录地点', dataIndex: 'address', ellipsis: true, tooltip: true },
|
||||
{ title: '浏览器', dataIndex: 'browser', ellipsis: true, tooltip: true },
|
||||
{ title: '终端系统', dataIndex: 'os', ellipsis: true, tooltip: true },
|
||||
{ title: '登录时间', dataIndex: 'loginTime', width: 180 },
|
||||
{ title: '最后活跃时间', dataIndex: 'lastActiveTime', width: 180 },
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
slotName: 'action',
|
||||
align: 'center',
|
||||
fixed: !isMobile() ? 'right' : undefined,
|
||||
show: has.hasPermOr(['monitor:online:kickout']),
|
||||
},
|
||||
]
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
queryForm.nickname = undefined
|
||||
queryForm.loginTime = undefined
|
||||
search()
|
||||
}
|
||||
|
||||
// 强退
|
||||
const handleKickout = (token: string) => {
|
||||
kickout(token).then(() => {
|
||||
search()
|
||||
Message.success('强退成功')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user