feat: 获取Redis中活跃用户

This commit is contained in:
bunny 2025-05-04 19:28:50 +08:00
parent 88857fd01d
commit 42647df377
31 changed files with 270 additions and 45 deletions

View File

@ -261,7 +261,7 @@ docker compose up -d
- [x] 系统监控后端返回403停止请求
- [x] 优化用户配置权限逻辑,配置后热更新逻辑等
- [x] 完善后端注释有需要添加ReadMe文档
- [ ] Redis中获取活跃用户
- [x] 获取Redis中活跃用户
## 前后端接口规范

View File

@ -110,3 +110,8 @@ export const updateUserinfo = (data: any) => {
export const updateUserPassword = (data: any) => {
return http.request<BaseResult<object>>('put', 'user/private/update/password', { data }, { headers: { 'Content-Type': 'multipart/form-data' } });
};
/* 查询缓存中的已登录的用户 */
export const getCacheLoggedInPage = (data: any) => {
return http.request<BaseResult<any>>('get', `user/getCacheUserPage/${data.currentPage}/${data.pageSize}`, { data });
};

View File

@ -1,8 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { loadUserinfoById } from '@/api/v1/system/adminUser';
import userAvatarIcon from '@/assets/svg/user_avatar.svg?component';
import { $t } from '@/plugins/i18n';
import { onMounted, ref } from 'vue';
const props = defineProps({
userId: { type: String as PropType<String> },
@ -60,8 +60,8 @@ onMounted(() => {
<el-descriptions-item :label="$t('email')">{{ userinfo.email }}</el-descriptions-item>
<el-descriptions-item :label="$t('phone')">{{ userinfo.phone }}</el-descriptions-item>
<el-descriptions-item :label="$t('sex')">
<el-tag v-if="userinfo.sex === 1"></el-tag>
<el-tag v-if="userinfo.sex === 0" type="danger"></el-tag>
<el-tag v-if="userinfo.sex === 1">{{ $t('man') }}</el-tag>
<el-tag v-if="userinfo.sex === 0" type="danger">{{ $t('female') }}</el-tag>
</el-descriptions-item>
<el-descriptions-item :label="$t('personDescription')" span="3">

View File

@ -1,5 +1,6 @@
import UserinfoDialog from '@/components/Table/Userinfo/UserinfoDialog.vue';
import { addDialog } from '@/components/ReDialog/index';
import UserinfoDialog from '@/components/Table/Userinfo/UserinfoDialog.vue';
import { $t } from '@/plugins/i18n';
/**
* *
@ -7,7 +8,7 @@ import { addDialog } from '@/components/ReDialog/index';
*/
export const selectUserinfo = async (userId: string) => {
addDialog({
title: '查看用户信息',
title: $t('view_user_info'),
draggable: true,
contentRenderer: (): JSX.Element => <UserinfoDialog userId={userId} />,
});

View File

@ -0,0 +1,36 @@
import { defineStore } from 'pinia';
import { pageSizes } from '@/enums/baseConstant';
import { getCacheLoggedInPage } from '@/api/v1/system/adminUser';
import { storePagination } from '@/store/useStorePagination';
export const useLoggedInStore = defineStore('loggedInStore', {
state() {
return {
datalist: [],
loading: false,
pagination: {
currentPage: 1,
pageSize: 30,
total: 1,
pageSizes,
},
};
},
getters: {},
actions: {
/* 查詢已登錄用戶 */
async fetchLoggedInPage() {
// 整理请求参数
const data = { ...this.pagination };
delete data.pageSizes;
delete data.total;
// 获取调度任务执行日志列表
const result = await getCacheLoggedInPage(data);
// 公共页面函数hook
const pagination = storePagination.bind(this);
return pagination(result);
},
},
});

View File

@ -46,7 +46,6 @@ export const useQuartzExecuteLogStore = defineStore('quartzExecuteLogStore', {
const data = { ...this.pagination, ...this.form };
delete data.pageSizes;
delete data.total;
delete data.background;
// 获取调度任务执行日志列表
const result = await getScheduleExecuteLogPage(data);

View File

@ -10,7 +10,7 @@ import AccountManagementIcon from '~icons/ri/profile-line';
import AccountManagement from '@/views/account-settings/components/account-management.vue';
export const columns: TableColumnList = [
{ type: 'index', index: (index: number) => index + 1, label: '序号', width: 60 },
{ type: 'index', index: (index: number) => index + 1, label: $t("index"), width: 60 },
// 用户名
{ label: $t('userLoginLog_username'), prop: 'username', width: 180 },
// 登录Ip

View File

@ -8,7 +8,7 @@ export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: '序号',
label: $t("index"),
width: 60,
},
// 模板名称

View File

@ -7,7 +7,7 @@ export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: '序号',
label: $t("index"),
width: 60,
},
// 邮箱

View File

@ -7,7 +7,7 @@ export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: '序号',
label: $t("index"),
width: 60,
},
// icon 类名

View File

@ -8,7 +8,7 @@ export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: '序号',
label: $t("index"),
width: 60,
},
{ label: $t('i18n.keyName'), prop: 'keyName' },

View File

@ -7,7 +7,7 @@ export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: '序号',
label: $t("index"),
width: 60,
},
{ label: $t('i18n_typeName'), prop: 'typeName' },

View File

@ -21,9 +21,9 @@ const timer = ref(null);
const { t } = useI18n();
const ruleForm = reactive({
username: '1319900154@qq.com',
username: 'admin@qq.com',
password: 'admin123',
emailCode: '1',
emailCode: '',
type: currentPage.value,
});

View File

@ -11,6 +11,7 @@ import { getTopMenu, initRouter } from '@/router/utils';
import Motion from '../utils';
import { ElMessage, FormInstance } from 'element-plus';
import { currentPage, formRules } from '@/views/login/utils';
import { $t } from '../../../plugins/i18n';
const router = useRouter();
const userStore = useUserStore();
@ -93,7 +94,7 @@ onBeforeUnmount(() => {
<!-- 邮箱登录 -->
<Motion :delay="300">
<el-form-item>
<el-button class="w-full" size="default" @click="currentPage = 'email'">邮箱登录</el-button>
<el-button class="w-full" size="default" @click="currentPage = 'email'">{{ $t('email_login') }}</el-button>
</el-form-item>
</Motion>
</el-form>

View File

@ -7,7 +7,7 @@ export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: '序号',
label: $t("index"),
width: 60,
},
// 消息标题

View File

@ -8,7 +8,7 @@ export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: '序号',
label: $t("index"),
width: 60,
},
// 消息标题

View File

@ -8,7 +8,7 @@ export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: '序号',
label: $t("index"),
width: 60,
},
// 是否启用

View File

@ -0,0 +1,82 @@
<script lang="ts" setup>
import { useLoggedInStore } from '@/store/monitor/loggedIn';
import { onMounted, ref } from 'vue';
import { $t } from '@/plugins/i18n';
import { PureTableBar } from '@/components/RePureTableBar';
import PureTable from '@pureadmin/table';
import { storeToRefs } from 'pinia';
import { columns } from '@/views/monitor/logged-in/utils/columns';
import { selectUserinfo } from '@/components/Table/Userinfo/columns';
import { useRenderIcon } from '@/components/ReIcon/src/hooks';
import Airplane from '@/assets/svg/airplane.svg';
import { onForcedOffline } from '@/views/system/admin-user/utils';
const tableRef = ref();
const loggedInStore = useLoggedInStore();
const { datalist, loading, pagination } = storeToRefs(loggedInStore);
const onSearch = async () => {
loggedInStore.loading = true;
await loggedInStore.fetchLoggedInPage();
loggedInStore.loading = false;
};
/** 当前页改变时 */
const onCurrentPageChange = async (value: number) => {
loggedInStore.pagination.currentPage = value;
await onSearch();
};
/** 当分页发生变化 */
const onPageSizeChange = async (value: number) => {
loggedInStore.pagination.pageSize = value;
await onSearch();
};
onMounted(() => {
onSearch();
});
</script>
<template>
<div class="main">
<PureTableBar :columns="columns" :title="$t('logged_in_user')" @fullscreen="tableRef.setAdaptive()" @refresh="onSearch">
<template v-slot="{ size, dynamicColumns }">
<pure-table
ref="tableRef"
:adaptiveConfig="{ offsetBottom: 96 }"
:columns="dynamicColumns"
:data="datalist"
:header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)',
}"
:loading="loading"
:pagination="pagination"
:size="size"
adaptive
align-whole="center"
border
highlight-current-row
row-key="id"
showOverflowTooltip
table-layout="auto"
@page-size-change="onPageSizeChange"
@page-current-change="onCurrentPageChange"
>
<template #username="{ row }">
<el-button v-show="row.username" link type="primary" @click="selectUserinfo(row.createUser)">
{{ row.username }}
</el-button>
</template>
<template #operation="{ row }">
<el-button :icon="useRenderIcon(Airplane)" :size="size" link type="primary" @click="onForcedOffline(row)">
{{ $t('forced_offline') }}
</el-button>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</template>

View File

@ -0,0 +1,25 @@
import { $t } from '@/plugins/i18n';
export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: $t("index"),
width: 60,
},
{ label: $t('adminUser_nickname'), prop: 'nickname', minWidth: 120 },
{ label: $t('adminUser_username'), prop: 'username', slot: 'username', minWidth: 120 },
{ label: $t('adminUser_email'), prop: 'email', minWidth: 120 },
{ label: $t('adminUser_phone'), prop: 'phone', minWidth: 120 },
{ label: $t('adminUser_summary'), prop: 'personDescription', minWidth: 150 },
{ label: $t('lastLoginIp'), prop: 'ipAddress', minWidth: 100 },
{ label: $t('lastLoginIpAddress'), prop: 'ipRegion', minWidth: 100 },
{ label: $t('expires'), prop: 'expires', minWidth: 120 },
{ label: $t('readMeDay'), prop: 'readMeDay', width: 100 },
{
label: $t('table.operation'),
fixed: 'right',
width: 120,
slot: 'operation',
},
];

View File

@ -7,7 +7,7 @@ export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: '序号',
label: $t("index"),
width: 60,
},
// 任务名称

View File

@ -7,7 +7,7 @@ export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: '序号',
label: $t("index"),
width: 60,
},
// 用户名

View File

@ -7,7 +7,7 @@ export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: '序号',
label: $t("index"),
width: 60,
},
// 分组名称

View File

@ -6,7 +6,7 @@ export const columns: TableColumnList = [
{
type: 'index',
index: (index: number) => index + 1,
label: '序号',
label: $t("index"),
width: 60,
},
// 任务名称

View File

@ -18,7 +18,7 @@ export async function onSearch() {
}
/** 添加部门 */
export function onAdd(parentId: number = 0) {
export function onAdd(parentId: string = '0') {
addDialog({
title: `${$t('addNew')}${$t('dept')}`,
props: {

View File

@ -3,7 +3,7 @@ import { onMounted, ref } from 'vue';
import { FormInstance, genFileId, UploadProps, UploadRawFile } from 'element-plus';
import { addRules, FormProps, uploadRules } from '@/views/system/files/utils';
import { $t } from '@/plugins/i18n';
import { useFilesStore } from '@/store/monitor/files';
import { useFilesStore } from '@/store/system/files';
import { UploadFilled } from '@element-plus/icons-vue';
const props = withDefaults(defineProps<FormProps>(), {
@ -28,10 +28,7 @@ const upload = ref();
const form = ref(props.formInline);
const filesStore = useFilesStore();
/**
* * 修改时替换之前文件
* @param files
*/
/* 修改时替换之前文件 */
const handleExceed: UploadProps['onExceed'] = (files) => {
upload.value!.clearFiles();
const file = files[0] as UploadRawFile;

View File

@ -5,7 +5,7 @@ import { PureTableBar } from '@/components/RePureTableBar';
import { selectUserinfo } from '@/components/Table/Userinfo/columns';
import { $t } from '@/plugins/i18n';
import { hasAuth } from '@/router/utils';
import { useFilesStore } from '@/store/monitor/files';
import { useFilesStore } from '@/store/system/files';
import { auth, columns, onAdd, onDelete, onDeleteBatch, onDownload, onDownloadBatch, onSearch, onUpdate, selectRows } from '@/views/system/files/utils';
import PureTable from '@pureadmin/table';
import { FormInstance } from 'element-plus';

View File

@ -1,7 +1,7 @@
import { addDialog } from '@/components/ReDialog/index';
import FilesDialog from '@/views/system/files/components/files-dialog.vue';
import { useFilesStore } from '@/store/monitor/files';
import { h, ref, VNode } from 'vue';
import { useFilesStore } from '@/store/system/files';
import { h, ref } from 'vue';
import { messageBox } from '@/utils/message';
import type { FormItemProps } from '@/views/system/files/utils/types';
import { $t } from '@/plugins/i18n';
@ -11,7 +11,7 @@ import type { UploadFiles } from 'element-plus';
// 选择的row列表
export const selectRows = ref([]);
export const formRef = ref<VNode | null>(null);
export const formRef = ref();
const filesStore = useFilesStore();
/** 搜索初始化系统文件 */
@ -28,17 +28,29 @@ export function onAdd() {
props: {
formInline: {
filename: undefined,
fileType: undefined,
filepath: undefined,
downloadCount: 0,
files: [],
isAdd: false,
isUpload: false,
},
},
draggable: true,
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => {
const dialog = h(FilesDialog, {}, []);
const dialog = h(FilesDialog, {
ref: formRef,
formInline: {
filename: undefined,
fileType: undefined,
filepath: undefined,
downloadCount: 0,
files: [],
isUpload: false,
},
});
formRef.value = dialog;
return dialog;
},
@ -83,7 +95,17 @@ export function onUpdate(row: any) {
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => {
const dialog = h(FilesDialog, { ref: formRef }, []);
const dialog = h(FilesDialog, {
ref: formRef,
formInline: {
filename: row.filename,
fileType: row.fileType,
filepath: row.filepath,
downloadCount: row.downloadCount,
isUpload: true,
files: undefined,
},
});
formRef.value = dialog;
return dialog;
},

View File

@ -40,8 +40,8 @@ function onAdd(parentId: any = 0) {
icon: '',
id: '',
extraIcon: '',
enterTransition: 'fade-transform',
leaveTransition: 'fade-transform',
enterTransition: '',
leaveTransition: '',
activePath: '',
redirect: '',
roles: [],
@ -58,7 +58,35 @@ function onAdd(parentId: any = 0) {
draggable: true,
closeOnClickModal: false,
fullscreenIcon: true,
contentRenderer: () => h(EditForm, { ref: dialogFormRef }),
contentRenderer: () =>
h(EditForm, {
ref: dialogFormRef,
formInline: {
menuType: 0,
higherMenuOptions: formatHigherMenuOptions(cloneDeep(menuStore.datalist)),
parentId,
title: '',
name: '',
path: '',
component: '',
rank: 99,
icon: '',
id: '',
extraIcon: '',
enterTransition: '',
leaveTransition: '',
activePath: '',
redirect: '',
roles: [],
frameSrc: '',
frameLoading: true,
keepAlive: false,
hiddenTag: false,
fixedTag: false,
showLink: true,
showParent: true,
},
}),
beforeSure: (done, { options }) => {
const menuFormRef = dialogFormRef.value.menuFormRef;
const curData = options.props.formInline as FormItemProps;
@ -115,7 +143,36 @@ function onUpdate(row?: FormItemProps) {
fullscreen: deviceDetection(),
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(EditForm, { ref: dialogFormRef }),
contentRenderer: () =>
h(EditForm, {
ref: dialogFormRef,
formInline: {
id: row?.id,
menuType: row?.menuType,
higherMenuOptions: formatHigherMenuOptions(cloneDeep(menuStore.datalist)),
parentId: row?.parentId,
title: row?.title,
name: row?.name,
path: row?.path,
component: row?.component,
rank: row?.rank,
icon: row?.icon,
frameSrc: row?.frameSrc,
extraIcon: row?.extraIcon,
// 因为要使用动画需要在前面加上 animate__ 修改时需要手动去除
enterTransition: row?.enterTransition?.replace('animate__', ''),
// 因为要使用动画需要在前面加上 animate__ 修改时需要手动去除
leaveTransition: row?.leaveTransition?.replace('animate__', ''),
activePath: row?.activePath,
frameLoading: row?.frameLoading,
keepAlive: row?.keepAlive,
hiddenTag: row?.hiddenTag,
fixedTag: row?.fixedTag,
showLink: row?.showLink,
showParent: row?.showParent,
redirect: row?.redirect,
},
}),
beforeSure: (done, { options }) => {
const menuFormRef = dialogFormRef.value.menuFormRef;
const curData = options.props.formInline as FormItemProps;

View File

@ -175,7 +175,7 @@ onMounted(() => {
</el-button>
<el-dropdown v-if="hasAuth(auth.update)" class="ml-1" type="primary">
<el-button :icon="useRenderIcon(More)" plain type="primary">更多操作</el-button>
<el-button :icon="useRenderIcon(More)" plain type="primary">{{ $t('more_actions') }}</el-button>
<template #dropdown>
<el-dropdown-menu>
<!-- 批量更新父级id -->

View File

@ -1,10 +1,10 @@
export const auth = {
// 分页查询
query: ['power::query', 'power::queryPage'],
query: 'permission::page',
// // 添加操作
add: ['power::addPower'],
add: ['permission::add'],
// 更新操作
update: ['power::updatePower'],
update: ['permission::update'],
// 删除操作
delete: ['power::delete'],
delete: ['permission::delete'],
};