Compare commits
3 Commits
608eaf9145
...
e5ad68f1ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5ad68f1ff | ||
|
|
27f9622b17 | ||
|
|
99ef1f1f61 |
@ -64,6 +64,12 @@ export const PERMISSIONS = {
|
|||||||
UPDATE: 'system:sysPicture:update',
|
UPDATE: 'system:sysPicture:update',
|
||||||
DELETE: 'system:sysPicture:delete',
|
DELETE: 'system:sysPicture:delete',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 在线用户监控权限
|
||||||
|
ONLINE: {
|
||||||
|
LIST: 'monitor:online:list',
|
||||||
|
LOGOUT: 'monitor:online:logout',
|
||||||
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 权限类型推断
|
// 权限类型推断
|
||||||
@ -112,6 +118,12 @@ export const PERMISSION_GROUPS = {
|
|||||||
PERMISSIONS.PICTURE.DELETE,
|
PERMISSIONS.PICTURE.DELETE,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 在线用户监控相关权限
|
||||||
|
ONLINE_MANAGEMENT: [
|
||||||
|
PERMISSIONS.ONLINE.LIST,
|
||||||
|
PERMISSIONS.ONLINE.LOGOUT,
|
||||||
|
],
|
||||||
|
|
||||||
// 系统管理员权限(包含所有权限)
|
// 系统管理员权限(包含所有权限)
|
||||||
SYSTEM_ADMIN: [
|
SYSTEM_ADMIN: [
|
||||||
...Object.values(PERMISSIONS.USER),
|
...Object.values(PERMISSIONS.USER),
|
||||||
@ -121,5 +133,6 @@ export const PERMISSION_GROUPS = {
|
|||||||
...Object.values(PERMISSIONS.OPER_LOG),
|
...Object.values(PERMISSIONS.OPER_LOG),
|
||||||
...Object.values(PERMISSIONS.FILE),
|
...Object.values(PERMISSIONS.FILE),
|
||||||
...Object.values(PERMISSIONS.PICTURE),
|
...Object.values(PERMISSIONS.PICTURE),
|
||||||
|
...Object.values(PERMISSIONS.ONLINE),
|
||||||
],
|
],
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
33
src/service/api/monitor/online/index.ts
Normal file
33
src/service/api/monitor/online/index.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { request } from '@/service/http'
|
||||||
|
import type {
|
||||||
|
OnlineUserCountVo,
|
||||||
|
OnlineUserSearchForm,
|
||||||
|
OnlineUserVo,
|
||||||
|
PageOnlineUserVo,
|
||||||
|
SysUserOnlineQueryBo,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
// 重新导出类型定义
|
||||||
|
export type { OnlineUserCountVo, OnlineUserSearchForm, OnlineUserVo, PageOnlineUserVo, SysUserOnlineQueryBo }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询在线用户列表
|
||||||
|
*/
|
||||||
|
export function getOnlineUserListPage(params: SysUserOnlineQueryBo) {
|
||||||
|
return request.Get<Service.ResponseResult<PageOnlineUserVo>>('/coder/sysUserOnline/listPage', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制注销
|
||||||
|
* @param userId 用户ID
|
||||||
|
*/
|
||||||
|
export function logoutUser(userId: string) {
|
||||||
|
return request.Get<Service.ResponseResult<string>>(`/coder/sysUserOnline/logout/${userId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取在线用户统计信息
|
||||||
|
*/
|
||||||
|
export function getOnlineUserCount() {
|
||||||
|
return request.Get<Service.ResponseResult<OnlineUserCountVo>>('/coder/sysUserOnline/count')
|
||||||
|
}
|
||||||
97
src/service/api/monitor/online/types.ts
Normal file
97
src/service/api/monitor/online/types.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* 在线用户监控 - 类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线用户实体
|
||||||
|
*/
|
||||||
|
export interface OnlineUserVo {
|
||||||
|
/** 用户ID */
|
||||||
|
userId: string
|
||||||
|
/** 登录名称 */
|
||||||
|
loginName: string
|
||||||
|
/** 用户名 */
|
||||||
|
userName: string
|
||||||
|
/** 用户头像 */
|
||||||
|
avatar?: string
|
||||||
|
/** 性别[1-男 2-女 3-未知] */
|
||||||
|
sex?: string
|
||||||
|
/** 手机号 */
|
||||||
|
phone?: string
|
||||||
|
/** 邮箱 */
|
||||||
|
email?: string
|
||||||
|
/** 用户类型[1-系统用户 2-注册用户 3-微信用户] */
|
||||||
|
userType?: string
|
||||||
|
/** 城市ID[可新增城市表-暂时无用] */
|
||||||
|
cityId?: string
|
||||||
|
/** 登录时间 */
|
||||||
|
loginTime?: string
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime?: string
|
||||||
|
/** 登录IP */
|
||||||
|
loginIp?: string
|
||||||
|
/** 登录地址 */
|
||||||
|
loginAddress?: string
|
||||||
|
/** 浏览器类型 */
|
||||||
|
browser?: string
|
||||||
|
/** 操作系统 */
|
||||||
|
os?: string
|
||||||
|
/** 设备名字 */
|
||||||
|
deviceName?: string
|
||||||
|
/** 是否超级管理员 */
|
||||||
|
isCoderAdmin?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线用户查询参数
|
||||||
|
*/
|
||||||
|
export interface SysUserOnlineQueryBo {
|
||||||
|
/** 页码 */
|
||||||
|
pageNo?: number
|
||||||
|
/** 页大小 */
|
||||||
|
pageSize?: number
|
||||||
|
/** 登录名称 */
|
||||||
|
loginName?: string
|
||||||
|
/** 用户名字 */
|
||||||
|
userName?: string
|
||||||
|
/** IP地址 */
|
||||||
|
loginIp?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线用户搜索表单
|
||||||
|
*/
|
||||||
|
export interface OnlineUserSearchForm {
|
||||||
|
/** 登录名称 */
|
||||||
|
loginName?: string
|
||||||
|
/** 用户名字 */
|
||||||
|
userName?: string
|
||||||
|
/** IP地址 */
|
||||||
|
loginIp?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页在线用户响应
|
||||||
|
*/
|
||||||
|
export interface PageOnlineUserVo {
|
||||||
|
/** 总记录数 */
|
||||||
|
total: number
|
||||||
|
/** 当前页 */
|
||||||
|
current: number
|
||||||
|
/** 每页大小 */
|
||||||
|
size: number
|
||||||
|
/** 总页数 */
|
||||||
|
pages: number
|
||||||
|
/** 数据列表 */
|
||||||
|
records: OnlineUserVo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线用户统计响应
|
||||||
|
*/
|
||||||
|
export interface OnlineUserCountVo {
|
||||||
|
/** 在线用户总数 */
|
||||||
|
onlineCount: number
|
||||||
|
/** 统计时间戳 */
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
688
src/views/monitor/online/index.vue
Normal file
688
src/views/monitor/online/index.vue
Normal file
@ -0,0 +1,688 @@
|
|||||||
|
<template>
|
||||||
|
<div class="online-user-management p-1 bg-gray-50 h-screen flex flex-col">
|
||||||
|
<!-- 搜索表单和在线用户表格 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-100 flex-1 flex flex-col overflow-hidden">
|
||||||
|
<!-- 搜索表单 -->
|
||||||
|
<div class="px-4 py-2 border-b border-gray-100">
|
||||||
|
<n-form
|
||||||
|
ref="searchFormRef"
|
||||||
|
:model="searchForm"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="auto"
|
||||||
|
class="search-form"
|
||||||
|
>
|
||||||
|
<n-grid :cols="4" :x-gap="8" :y-gap="4">
|
||||||
|
<n-grid-item>
|
||||||
|
<n-form-item label="登录账号" path="loginName">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchForm.loginName"
|
||||||
|
placeholder="请输入登录账号"
|
||||||
|
clearable
|
||||||
|
@keydown.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<n-grid-item>
|
||||||
|
<n-form-item label="用户姓名" path="userName">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchForm.userName"
|
||||||
|
placeholder="请输入用户姓名"
|
||||||
|
clearable
|
||||||
|
@keydown.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<n-grid-item>
|
||||||
|
<n-form-item label="登录IP" path="loginIp">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchForm.loginIp"
|
||||||
|
placeholder="请输入登录IP"
|
||||||
|
clearable
|
||||||
|
@keydown.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<n-grid-item>
|
||||||
|
<n-form-item label="" path="">
|
||||||
|
<NSpace>
|
||||||
|
<NButton type="primary" @click="handleSearch">
|
||||||
|
<template #icon>
|
||||||
|
<NIcon><icon-park-outline:search /></NIcon>
|
||||||
|
</template>
|
||||||
|
搜索
|
||||||
|
</NButton>
|
||||||
|
<NButton @click="handleReset">
|
||||||
|
<template #icon>
|
||||||
|
<NIcon><IconParkOutlineRefresh /></NIcon>
|
||||||
|
</template>
|
||||||
|
重置
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
</n-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计信息和操作栏 -->
|
||||||
|
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- 在线用户统计 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<NIcon size="16" color="#18a058">
|
||||||
|
<icon-park-outline:people />
|
||||||
|
</NIcon>
|
||||||
|
<span class="text-sm font-medium text-gray-700">在线用户:</span>
|
||||||
|
<NTag type="primary" size="small">
|
||||||
|
{{ onlineCount }} 人
|
||||||
|
</NTag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自动刷新开关 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-600">自动刷新:</span>
|
||||||
|
<NSwitch
|
||||||
|
v-model:value="autoRefreshEnabled"
|
||||||
|
size="small"
|
||||||
|
@update:value="handleAutoRefreshToggle"
|
||||||
|
/>
|
||||||
|
<span v-if="autoRefreshEnabled" class="text-xs text-gray-500">
|
||||||
|
({{ refreshCountdown }}s)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最后更新时间 -->
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
最后更新: {{ lastUpdateTime }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4 text-sm text-gray-500">
|
||||||
|
<span>共 {{ pagination.itemCount }} 条</span>
|
||||||
|
<NButton text @click="getOnlineUserList">
|
||||||
|
<template #icon>
|
||||||
|
<NIcon><IconParkOutlineRefresh /></NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格内容 -->
|
||||||
|
<div class="table-wrapper flex-1 overflow-auto pt-4">
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<n-data-table
|
||||||
|
v-if="tableData.length > 0 || loading"
|
||||||
|
:columns="columns"
|
||||||
|
:data="tableData"
|
||||||
|
:loading="loading"
|
||||||
|
:row-key="(row: OnlineUserVo) => row.userId"
|
||||||
|
:bordered="false"
|
||||||
|
:single-line="false"
|
||||||
|
:scroll-x="1650"
|
||||||
|
size="medium"
|
||||||
|
class="custom-table"
|
||||||
|
@update:checked-row-keys="handleRowSelectionChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<CoiEmpty
|
||||||
|
v-else
|
||||||
|
:type="getEmptyType()"
|
||||||
|
:title="getEmptyTitle()"
|
||||||
|
:description="getEmptyDescription()"
|
||||||
|
:show-action="true"
|
||||||
|
:action-text="getEmptyActionText()"
|
||||||
|
size="medium"
|
||||||
|
@action="handleEmptyAction"
|
||||||
|
>
|
||||||
|
<template #action>
|
||||||
|
<NButton
|
||||||
|
type="primary"
|
||||||
|
size="medium"
|
||||||
|
@click="handleEmptyAction"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon>
|
||||||
|
<IconParkOutlineRefresh v-if="hasSearchConditions()" />
|
||||||
|
<icon-park-outline:refresh v-else />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
{{ getEmptyActionText() }}
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
</CoiEmpty>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页器 -->
|
||||||
|
<div v-if="tableData.length > 0" class="border-t border-gray-100">
|
||||||
|
<CoiPagination
|
||||||
|
v-model:page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:item-count="pagination.itemCount"
|
||||||
|
:show-size-picker="true"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:show-quick-jumper="true"
|
||||||
|
:show-current-info="true"
|
||||||
|
@update:page="handlePageChange"
|
||||||
|
@update:page-size="handlePageSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { h, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import type { DataTableColumns, FormInst } from 'naive-ui'
|
||||||
|
import { NButton, NIcon, NPopconfirm, NSpace, NSwitch, NTag } from 'naive-ui'
|
||||||
|
import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
|
||||||
|
import IconParkOutlineRefresh from '~icons/icon-park-outline/refresh'
|
||||||
|
import CoiEmpty from '@/components/common/CoiEmpty.vue'
|
||||||
|
import CoiPagination from '@/components/common/CoiPagination.vue'
|
||||||
|
import {
|
||||||
|
getOnlineUserCount,
|
||||||
|
getOnlineUserListPage,
|
||||||
|
logoutUser,
|
||||||
|
} from '@/service/api/monitor/online'
|
||||||
|
import type { OnlineUserSearchForm, OnlineUserVo } from '@/service/api/monitor/online'
|
||||||
|
import { coiMsgError, coiMsgSuccess } from '@/utils/coi'
|
||||||
|
import { PERMISSIONS } from '@/constants/permissions'
|
||||||
|
import { usePermission } from '@/hooks'
|
||||||
|
|
||||||
|
// 权限相关
|
||||||
|
const { hasButton } = usePermission()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableData = ref<OnlineUserVo[]>([])
|
||||||
|
const searchFormRef = ref<FormInst | null>(null)
|
||||||
|
const onlineCount = ref(0)
|
||||||
|
const lastUpdateTime = ref('')
|
||||||
|
|
||||||
|
// 自动刷新相关
|
||||||
|
const autoRefreshEnabled = ref(false)
|
||||||
|
const refreshInterval = ref<NodeJS.Timeout | null>(null)
|
||||||
|
const refreshCountdown = ref(30)
|
||||||
|
const countdownInterval = ref<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
|
// 分页数据
|
||||||
|
const pagination = ref({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
itemCount: 0,
|
||||||
|
showSizePicker: true,
|
||||||
|
pageSizes: [10, 20, 50, 100],
|
||||||
|
})
|
||||||
|
|
||||||
|
// 搜索表单数据
|
||||||
|
const searchForm = ref<OnlineUserSearchForm>({})
|
||||||
|
|
||||||
|
// 选中的行数据
|
||||||
|
const selectedRows = ref<OnlineUserVo[]>([])
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns: DataTableColumns<OnlineUserVo> = [
|
||||||
|
{
|
||||||
|
type: 'selection',
|
||||||
|
width: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '序号',
|
||||||
|
key: 'index',
|
||||||
|
width: 70,
|
||||||
|
align: 'center',
|
||||||
|
render: (_, index) => {
|
||||||
|
return (pagination.value.page - 1) * pagination.value.pageSize + index + 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '头像',
|
||||||
|
key: 'avatar',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
render: (row) => {
|
||||||
|
return h('div', { class: 'flex justify-center' }, [
|
||||||
|
h('div', {
|
||||||
|
class: 'w-10 h-10 rounded-full overflow-hidden border-2 border-gray-200',
|
||||||
|
}, [
|
||||||
|
row.avatar
|
||||||
|
? h('img', {
|
||||||
|
src: row.avatar,
|
||||||
|
alt: `${row.userName}的头像`,
|
||||||
|
class: 'w-full h-full object-cover',
|
||||||
|
onError: (e) => {
|
||||||
|
// 头像加载失败时显示默认头像
|
||||||
|
(e.target as HTMLElement).style.display = 'none'
|
||||||
|
const parent = (e.target as HTMLElement).parentElement
|
||||||
|
if (parent) {
|
||||||
|
parent.className = 'w-10 h-10 rounded-full bg-gradient-to-r from-blue-400 to-purple-500 flex items-center justify-center text-white text-sm font-bold shadow-md'
|
||||||
|
parent.textContent = row.loginName.charAt(0).toUpperCase()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: h('div', {
|
||||||
|
class: 'w-full h-full bg-gradient-to-r from-blue-400 to-purple-500 flex items-center justify-center text-white text-sm font-bold',
|
||||||
|
}, row.loginName.charAt(0).toUpperCase()),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '登录账号',
|
||||||
|
key: 'loginName',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
render: (row) => {
|
||||||
|
return h('span', { class: 'text-gray-600 font-medium' }, row.loginName)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户姓名',
|
||||||
|
key: 'userName',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
render: (row) => {
|
||||||
|
return h('div', { class: 'text-gray-600' }, row.userName)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '登录IP',
|
||||||
|
key: 'loginIp',
|
||||||
|
width: 130,
|
||||||
|
align: 'center',
|
||||||
|
render: (row) => {
|
||||||
|
return h('span', {
|
||||||
|
class: 'text-blue-600 font-mono text-sm bg-blue-50 px-2 py-1 rounded',
|
||||||
|
}, row.loginIp || '-')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '登录地址',
|
||||||
|
key: 'loginAddress',
|
||||||
|
width: 150,
|
||||||
|
align: 'center',
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render: (row) => {
|
||||||
|
return h('span', { class: 'text-gray-600' }, row.loginAddress || '-')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '浏览器',
|
||||||
|
key: 'browser',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render: (row) => {
|
||||||
|
return h('span', { class: 'text-gray-600 text-sm' }, row.browser || '-')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作系统',
|
||||||
|
key: 'os',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render: (row) => {
|
||||||
|
return h('span', { class: 'text-gray-600 text-sm' }, row.os || '-')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '登录设备',
|
||||||
|
key: 'deviceName',
|
||||||
|
width: 130,
|
||||||
|
align: 'center',
|
||||||
|
ellipsis: { tooltip: true },
|
||||||
|
render: (row) => {
|
||||||
|
return h('span', { class: 'text-gray-600 text-sm' }, row.deviceName || '-')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '登录时间',
|
||||||
|
key: 'loginTime',
|
||||||
|
width: 160,
|
||||||
|
align: 'center',
|
||||||
|
render: (row) => {
|
||||||
|
return h('span', { class: 'text-gray-500 text-sm' }, row.loginTime || '-')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 150,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
render: (row) => {
|
||||||
|
const buttons = []
|
||||||
|
|
||||||
|
// 强制注销按钮
|
||||||
|
if (hasButton(PERMISSIONS.ONLINE.LOGOUT)) {
|
||||||
|
buttons.push(h(NPopconfirm, {
|
||||||
|
onPositiveClick: () => handleLogout(row),
|
||||||
|
negativeText: '取消',
|
||||||
|
positiveText: '确定',
|
||||||
|
}, {
|
||||||
|
default: () => `确定要强制注销用户「${row.userName}」吗?`,
|
||||||
|
trigger: () => h(NButton, {
|
||||||
|
type: 'error',
|
||||||
|
secondary: true,
|
||||||
|
size: 'small',
|
||||||
|
class: 'action-btn-secondary action-btn-danger',
|
||||||
|
}, {
|
||||||
|
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
|
||||||
|
default: () => h(IconParkOutlineDelete),
|
||||||
|
}),
|
||||||
|
default: () => '强制注销',
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('div', { class: 'flex items-center justify-center gap-2' }, buttons)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 获取在线用户列表
|
||||||
|
async function getOnlineUserList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 构建请求参数,过滤掉空值
|
||||||
|
const filteredParams = Object.entries(searchForm.value).reduce((acc, [key, value]) => {
|
||||||
|
if (value !== null && value !== undefined && value !== '') {
|
||||||
|
acc[key] = value
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, any>)
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
pageNo: pagination.value.page,
|
||||||
|
pageSize: pagination.value.pageSize,
|
||||||
|
...filteredParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isSuccess, data } = await getOnlineUserListPage(params)
|
||||||
|
|
||||||
|
if (isSuccess && data) {
|
||||||
|
tableData.value = data.records || []
|
||||||
|
pagination.value.itemCount = data.total || 0
|
||||||
|
|
||||||
|
// 更新最后更新时间
|
||||||
|
lastUpdateTime.value = new Date().toLocaleString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('获取在线用户列表失败:', error)
|
||||||
|
coiMsgError('获取在线用户列表失败')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取在线用户统计
|
||||||
|
async function getOnlineUserCountData() {
|
||||||
|
try {
|
||||||
|
const { isSuccess, data } = await getOnlineUserCount()
|
||||||
|
if (isSuccess && data) {
|
||||||
|
onlineCount.value = data.onlineCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('获取在线用户统计失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制注销
|
||||||
|
async function handleLogout(row: OnlineUserVo) {
|
||||||
|
try {
|
||||||
|
const { isSuccess } = await logoutUser(row.userId)
|
||||||
|
if (isSuccess) {
|
||||||
|
coiMsgSuccess(`用户「${row.userName}」已被强制注销`)
|
||||||
|
await Promise.all([getOnlineUserList(), getOnlineUserCountData()])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
coiMsgError('强制注销失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('强制注销失败:', error)
|
||||||
|
coiMsgError('强制注销失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
async function handleSearch() {
|
||||||
|
pagination.value.page = 1
|
||||||
|
await getOnlineUserList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置处理
|
||||||
|
async function handleReset() {
|
||||||
|
searchForm.value = {}
|
||||||
|
pagination.value.page = 1
|
||||||
|
await getOnlineUserList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页变化处理
|
||||||
|
async function handlePageChange(page: number) {
|
||||||
|
pagination.value.page = page
|
||||||
|
await getOnlineUserList()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePageSizeChange(pageSize: number) {
|
||||||
|
pagination.value.pageSize = pageSize
|
||||||
|
pagination.value.page = 1
|
||||||
|
await getOnlineUserList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理行选择变化
|
||||||
|
function handleRowSelectionChange(rowKeys: (string | number)[]) {
|
||||||
|
selectedRows.value = tableData.value.filter(row => rowKeys.includes(row.userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动刷新处理
|
||||||
|
function handleAutoRefreshToggle(enabled: boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
startAutoRefresh()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
stopAutoRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAutoRefresh() {
|
||||||
|
stopAutoRefresh() // 先停止之前的定时器
|
||||||
|
|
||||||
|
refreshCountdown.value = 30
|
||||||
|
|
||||||
|
// 倒计时定时器
|
||||||
|
countdownInterval.value = setInterval(() => {
|
||||||
|
refreshCountdown.value--
|
||||||
|
if (refreshCountdown.value <= 0) {
|
||||||
|
refreshCountdown.value = 30
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// 刷新定时器
|
||||||
|
refreshInterval.value = setInterval(async () => {
|
||||||
|
await Promise.all([getOnlineUserList(), getOnlineUserCountData()])
|
||||||
|
}, 30000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAutoRefresh() {
|
||||||
|
if (refreshInterval.value) {
|
||||||
|
clearInterval(refreshInterval.value)
|
||||||
|
refreshInterval.value = null
|
||||||
|
}
|
||||||
|
if (countdownInterval.value) {
|
||||||
|
clearInterval(countdownInterval.value)
|
||||||
|
countdownInterval.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空状态处理
|
||||||
|
function getEmptyType() {
|
||||||
|
return hasSearchConditions() ? 'search' : 'empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmptyTitle() {
|
||||||
|
return hasSearchConditions() ? '未找到相关数据' : '暂无在线用户'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmptyDescription() {
|
||||||
|
return hasSearchConditions() ? '请尝试调整搜索条件' : '当前没有用户在线'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmptyActionText() {
|
||||||
|
return hasSearchConditions() ? '重新搜索' : '刷新数据'
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSearchConditions() {
|
||||||
|
const { loginName, userName, loginIp } = searchForm.value
|
||||||
|
return !!(loginName || userName || loginIp)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEmptyAction() {
|
||||||
|
if (hasSearchConditions()) {
|
||||||
|
await handleReset()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await getOnlineUserList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([getOnlineUserList(), getOnlineUserCountData()])
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopAutoRefresh()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 搜索表单样式 */
|
||||||
|
.search-form :deep(.n-form-item) {
|
||||||
|
margin-bottom: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.n-form-item .n-form-item-feedback-wrapper) {
|
||||||
|
min-height: 0 !important;
|
||||||
|
padding-top: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.n-form-item .n-form-item-label) {
|
||||||
|
padding-bottom: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.n-input),
|
||||||
|
.search-form :deep(.n-select) {
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.n-input .n-input__input-el) {
|
||||||
|
padding: 2px 1px !important;
|
||||||
|
min-height: 32px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式 */
|
||||||
|
.custom-table :deep(.n-data-table-td) {
|
||||||
|
padding: 8px 12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table :deep(.n-data-table-th) {
|
||||||
|
background-color: #fafafa;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table :deep(.n-data-table-tr:hover .n-data-table-td) {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table :deep(.n-data-table-tbody .n-data-table-tr) {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table :deep(.n-button--small-type) {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table :deep(.n-tag--small-type) {
|
||||||
|
height: 22px;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper::-webkit-scrollbar-corner {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table :deep(.n-data-table-base-table) {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
.action-btn-secondary {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-danger {
|
||||||
|
color: var(--error-color);
|
||||||
|
border-color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-danger:hover {
|
||||||
|
background-color: var(--error-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user