feat: 🚀 修改用户信息查看用户登录日志完成

This commit is contained in:
bunny 2024-10-22 15:55:48 +08:00
parent a9a2a140fb
commit becdc20745
10 changed files with 298 additions and 223 deletions

View File

@ -69,6 +69,11 @@ export const fetchAddAdminUser = (data: any) => {
return http.request<BaseResult<object>>('post', 'user/addAdminUser', { data });
};
/** 用户信息---更新本地用户信息 */
export const fetchUpdateAdminUserByLocalUser = (data: any) => {
return http.request<BaseResult<object>>('put', 'user/noManage/updateAdminUserByLocalUser', { data });
};
/** 用户信息---更新用户信息 */
export const fetchUpdateAdminUser = (data: any) => {
return http.request<BaseResult<object>>('put', 'user/updateAdminUser', { data });

View File

@ -6,12 +6,12 @@ export const fetchGetFilesList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `files/getFilesList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 系统文件管理---下载系统文件 */
/** 系统文件管理---根据I下载系统文件d */
export const downloadFilesByFileId = (data: any) => {
return http.request<any>('get', `files/downloadFilesByFileId/${data.id}`, { responseType: 'blob' });
};
/** 系统文件管理---下载系统文件 */
/** 系统文件管理---批量下载系统文件 */
export const downloadFilesByFilepath = (data: any) => {
return http.request<any>('get', `files/downloadFilesByFilepath`, { params: data, responseType: 'blob' });
};

View File

@ -9,14 +9,90 @@ const languageData = localStorage.getItem('i18nStore');
export const i18n = createI18n({
// 如果要支持 compositionAPI此项必须设置为 false
legacy: false,
// locale: 'zh',
fallbackLocale: 'en',
locale: 'zh',
fallbackLocale: 'zh',
// ? 全局注册$t方法
globalInjection: true,
// 本地内容存在时,首次加载如果本地存储没有多语言需要再刷新
messages: languageData ? JSON.parse(languageData).i18n : {},
});
/*const siphonI18n = (function () {
// 仅初始化一次国际化配置
let cache = Object.fromEntries(
Object.entries(import.meta.glob('../../locales/!*.y(a)?ml', { eager: true })).map(([key, value]: any) => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)[1];
return [matched, value.default];
}),
);
return (prefix = 'zh-CN') => {
return cache[prefix];
};
})();
/!** 获取对象中所有嵌套对象的key键并将它们用点号分割组成字符串 *!/
function getObjectKeys(obj) {
const stack = [];
const keys: Set<string> = new Set();
stack.push({ obj, key: '' });
while (stack.length > 0) {
const { obj, key } = stack.pop();
for (const k in obj) {
const newKey = key ? `${key}.${k}` : k;
if (obj[k] && isObject(obj[k])) {
stack.push({ obj: obj[k], key: newKey });
} else {
keys.add(key);
}
}
}
return keys;
}
/!** 将展开的key缓存 *!/
const keysCache: Map<string, Set<string>> = new Map();
const flatI18n = (prefix = 'zh') => {
let cache = keysCache.get(prefix);
if (!cache) {
cache = getObjectKeys(siphonI18n(prefix));
keysCache.set(prefix, cache);
}
return cache;
};
/!**
* locales文件夹下文件进行国际化匹配
* @param message message
* @returns message
*!/
export function transformI18n(message: any = '') {
if (!message) {
return '';
}
// 处理存储动态路由的title,格式 {zh:"",en:""}
if (typeof message === 'object') {
const locale: string | WritableComputedRef<string> | any = i18n.global.locale;
return message[locale?.value];
}
const key = message.match(/(\S*)\./)?.input;
if (key && flatI18n('zh-CN').has(key)) {
return i18n.global.t.call(i18n.global.locale, message);
} else if (!key && Object.hasOwn(siphonI18n('zh'), message)) {
// 兼容非嵌套形式的国际化写法
return i18n.global.t.call(i18n.global.locale, message);
} else {
return message;
}
}*/
export const $t: any = (i18n.global as any).t as any;
export function useI18n(app: App) {

View File

@ -2,7 +2,16 @@ import { defineStore } from 'pinia';
import { pageSizes } from '@/enums/baseConstant';
import { storeMessage } from '@/utils/message';
import { storePagination } from '@/store/useStorePagination';
import { fetchAddAdminUser, fetchDeleteAdminUser, fetchGetAdminUserList, fetchQueryUser, fetchUpdateAdminUser, fetchUpdateUserPasswordByAdmin, fetchUpdateUserStatusByAdmin } from '@/api/v1/adminUser';
import {
fetchAddAdminUser,
fetchDeleteAdminUser,
fetchGetAdminUserList,
fetchQueryUser,
fetchUpdateAdminUser,
fetchUpdateAdminUserByLocalUser,
fetchUpdateUserPasswordByAdmin,
fetchUpdateUserStatusByAdmin,
} from '@/api/v1/adminUser';
/**
* Store
@ -60,6 +69,7 @@ export const useAdminUserStore = defineStore('adminUserStore', {
return pagination(result);
},
/** 查询用户 */
async queryUser(data: any) {
const result = await fetchQueryUser(data);
if (result.code !== 200) return [];
@ -72,6 +82,12 @@ export const useAdminUserStore = defineStore('adminUserStore', {
return storeMessage(result);
},
/** 修改本地用户信息 */
async updateAdminUserByLocalUser(data: any) {
const result = await fetchUpdateAdminUserByLocalUser(data);
return storeMessage(result);
},
/** 修改用户信息 */
async updateAdminUser(data: any) {
const result = await fetchUpdateAdminUser(data);

View File

@ -2,53 +2,19 @@
import { useRouter } from 'vue-router';
import { onBeforeMount, ref } from 'vue';
import { Text } from '@/components/Text';
import Profile from './profile.vue';
import Preferences from './references.vue';
import SecurityLog from './security-log.vue';
import { deviceDetection, useGlobal } from '@pureadmin/utils';
import AccountManagement from './account-management.vue';
import { useDataThemeChange } from '@/layout/hooks/useDataThemeChange';
import LaySidebarTopCollapse from '@/layout/components/lay-sidebar/components/SidebarTopCollapse.vue';
import leftLine from '@iconify-icons/ri/arrow-left-s-line';
import ProfileIcon from '@iconify-icons/ri/user-3-line';
import PreferencesIcon from '@iconify-icons/ri/settings-3-line';
import SecurityLogIcon from '@iconify-icons/ri/window-line';
import AccountManagementIcon from '@iconify-icons/ri/profile-line';
import { $t } from '@/plugins/i18n';
import { userInfos } from '@/views/account-settings/utils/hooks';
import { panes } from '@/views/account-settings/utils/columns';
const router = useRouter();
const isOpen = ref(!deviceDetection());
const { $storage } = useGlobal<GlobalPropertiesApi>();
const witchPane = ref('profile');
const panes = [
{
key: 'profile',
label: '个人信息',
icon: ProfileIcon,
component: Profile,
},
{
key: 'preferences',
label: '偏好设置',
icon: PreferencesIcon,
component: Preferences,
},
{
key: 'securityLog',
label: '安全日志',
icon: SecurityLogIcon,
component: SecurityLog,
},
{
key: 'accountManagement',
label: '账户管理',
icon: AccountManagementIcon,
component: AccountManagement,
},
];
onBeforeMount(() => {
useDataThemeChange().dataThemeChange($storage.layout?.overallStyle);
});

View File

@ -3,110 +3,121 @@ import { onMounted, ref } from 'vue';
import { message } from '@/utils/message';
import type { FormInstance } from 'element-plus';
import ReCropperPreview from '@/components/CropperPreview';
import { createFormData, deviceDetection } from '@pureadmin/utils';
import { deviceDetection } from '@pureadmin/utils';
import uploadLine from '@iconify-icons/ri/upload-line';
import { useUserStore } from '@/store/system/user';
import { rules } from '@/views/account-settings/utils/columns';
import { onSearch, userInfos } from '@/views/account-settings/utils/hooks';
import { cropperBlob, handleSubmitImage, isShow, onSearchByUserinfo, uploadAvatarSrc, userInfos } from '@/views/account-settings/utils/hooks';
import { $t } from '@/plugins/i18n';
import { sexConstant } from '@/enums/baseConstant';
import { useAdminUserStore } from '@/store/system/adminUser';
const imgSrc = ref('');
const cropperBlob = ref();
const cropRef = ref();
const uploadRef = ref();
const isShow = ref(false);
const userInfoFormRef = ref<FormInstance>();
const userStore = useUserStore();
const uploadRef = ref();
const cropRef = ref();
// base64
const imgBase64Src = ref('');
function queryEmail(queryString, callback) {
const emailList = [{ value: '@qq.com' }, { value: '@126.com' }, { value: '@163.com' }];
let results = [];
let queryList = [];
emailList.map(item => queryList.push({ value: queryString.split('@')[0] + item.value }));
results = queryString ? queryList.filter(item => item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0) : queryList;
callback(results);
}
const onChange = uploadFile => {
const reader = new FileReader();
reader.onload = e => {
imgSrc.value = e.target.result as string;
isShow.value = true;
};
reader.readAsDataURL(uploadFile.raw);
};
const adminUserStore = useAdminUserStore();
/** 关闭弹窗 */
const handleClose = () => {
cropRef.value.hidePopover();
uploadRef.value.clearFiles();
isShow.value = false;
};
/** 剪裁头像 */
const onCropper = ({ blob }) => (cropperBlob.value = blob);
const handleSubmitImage = () => {
const formData = createFormData({
files: new File([cropperBlob.value], 'avatar'),
});
// formUpload(formData)
// .then(({ success, data }) => {
// if (success) {
// message('', { type: 'success' });
// handleClose();
// } else {
// message('');
// }
// })
// .catch(error => {
// message(` ${error}`, { type: 'error' });
// });
/** 头像修改内容 */
const onChange = (uploadFile: any) => {
const reader = new FileReader();
reader.onload = e => {
imgBase64Src.value = e.target.result as string;
isShow.value = true;
console.log(imgBase64Src.value);
};
reader.readAsDataURL(uploadFile.raw);
};
//
/** 提交表单 */
const onSubmit = async (formEl: FormInstance) => {
await formEl.validate((valid, fields) => {
await formEl.validate(async valid => {
if (valid) {
console.log(userInfos);
message('更新信息成功', { type: 'success' });
//
const avatar = uploadAvatarSrc.value;
if (avatar) userInfos.avatar = avatar;
await adminUserStore.updateAdminUserByLocalUser(userInfos);
await onSearchByUserinfo();
} else {
console.log('Error submit!', fields);
message($t('required_fields'), { type: 'warning' });
}
});
};
onMounted(() => {
onSearch();
onSearchByUserinfo();
});
</script>
<template>
<div :class="['min-w-[180px]', deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]']">
<h3 class="my-8">个人信息</h3>
<!-- 头像 -->
<el-form ref="userInfoFormRef" :model="userInfos" :rules="rules" label-position="top">
<el-form-item label="头像">
<el-form-item :label="$t('avatar')">
<el-avatar :size="80" :src="userInfos.avatar" />
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" :on-change="onChange" :show-file-list="false" accept="image/*" action="#">
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" :on-change="onChange" :show-file-list="false" accept="image/*">
<el-button class="ml-4" plain>
<IconifyIconOffline :icon="uploadLine" />
<span class="ml-2">更新头像</span>
<span class="ml-2">{{ $t('upload_avatar') }}</span>
</el-button>
<template #tip>
<div class="el-upload__tip text-red-600">{{ $t('upload_user_avatar_tip') }}</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="userInfos.nickname" placeholder="请输入昵称" />
<!-- 用户名 -->
<el-form-item :label="$t('adminUser_username')" prop="username">
<el-input v-model="userInfos.username" :placeholder="$t('adminUser_username')" autocomplete="off" type="text" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-autocomplete v-model="userInfos.email" :fetch-suggestions="queryEmail" :trigger-on-focus="false" class="w-full" clearable placeholder="请输入邮箱" />
<!-- 昵称 -->
<el-form-item :label="$t('adminUser_nickName')" prop="nickName">
<el-input v-model="userInfos.nickName" :placeholder="$t('adminUser_nickName')" autocomplete="off" type="text" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="userInfos.phone" clearable placeholder="请输入联系电话" />
<!-- 邮箱 -->
<el-form-item :label="$t('adminUser_email')" prop="email">
<el-input v-model="userInfos.email" :placeholder="$t('adminUser_email')" autocomplete="off" type="text" />
</el-form-item>
<el-form-item label="简介">
<el-input v-model="userInfos.description" :autosize="{ minRows: 6, maxRows: 8 }" maxlength="56" placeholder="请输入简介" show-word-limit type="textarea" />
<!-- 手机号 -->
<el-form-item :label="$t('adminUser_phone')" prop="phone">
<el-input v-model="userInfos.phone" :placeholder="$t('adminUser_phone')" autocomplete="off" type="text" />
</el-form-item>
<el-button type="primary" @click="onSubmit(userInfoFormRef)"> 更新信息</el-button>
<!-- 性别 -->
<el-form-item :label="$t('adminUser_sex')" prop="sex">
<el-select v-model="userInfos.sex" :placeholder="$t('adminUser_sex')" clearable filterable>
<el-option v-for="(item, index) in sexConstant" :key="index" :label="item.label" :navigationBar="false" :value="item.value" />
</el-select>
</el-form-item>
<!-- 用户简介 -->
<el-form-item :label="$t('adminUser_summary')" prop="summary">
<el-input v-model="userInfos.summary" :autosize="{ minRows: 3, maxRows: 6 }" :placeholder="$t('adminUser_summary')" autocomplete="off" maxlength="200" show-word-limit type="textarea" />
</el-form-item>
<!-- 更新信息 -->
<el-button type="primary" @click="onSubmit(userInfoFormRef)"> {{ $t('update_information') }}</el-button>
</el-form>
<el-dialog v-model="isShow" :before-close="handleClose" :closeOnClickModal="false" :fullscreen="deviceDetection()" destroy-on-close title="编辑头像" width="40%">
<ReCropperPreview ref="cropRef" :imgSrc="imgSrc" @cropper="onCropper" />
<ReCropperPreview ref="cropRef" :imgSrc="imgBase64Src" @cropper="onCropper" />
<template #footer>
<div class="dialog-footer">
<el-button bg text @click="handleClose">取消</el-button>

View File

@ -1,69 +0,0 @@
<script setup lang="ts">
import { ref } from "vue";
import { message } from "@/utils/message";
import { deviceDetection } from "@pureadmin/utils";
defineOptions({
name: "Preferences"
});
const list = ref([
{
title: "账户密码",
illustrate: "其他用户的消息将以站内信的形式通知",
checked: true
},
{
title: "系统消息",
illustrate: "系统消息将以站内信的形式通知",
checked: true
},
{
title: "待办任务",
illustrate: "待办任务将以站内信的形式通知",
checked: true
}
]);
function onChange(val, item) {
console.log("onChange", val);
message(`${item.title}设置成功`, { type: "success" });
}
</script>
<template>
<div
:class="[
'min-w-[180px]',
deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]'
]"
>
<h3 class="my-8">偏好设置</h3>
<div v-for="(item, index) in list" :key="index">
<div class="flex items-center">
<div class="flex-1">
<p>{{ item.title }}</p>
<p class="wp-4">
<el-text class="mx-1" type="info">
{{ item.illustrate }}
</el-text>
</p>
</div>
<el-switch
v-model="item.checked"
inline-prompt
active-text="是"
inactive-text="否"
@change="val => onChange(val, item)"
/>
</div>
<el-divider />
</div>
</div>
</template>
<style lang="scss" scoped>
.el-divider--horizontal {
border-top: 0.1px var(--el-border-color) var(--el-border-style);
}
</style>

View File

@ -1,41 +1,40 @@
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import { onMounted, reactive } from 'vue';
import { deviceDetection } from '@pureadmin/utils';
import { PaginationProps, PureTable } from '@pureadmin/table';
import { PureTable } from '@pureadmin/table';
import { columns } from '@/views/account-settings/utils/columns';
import { $t } from '@/plugins/i18n';
import { useUserLoginLogStore } from '@/store/monitor/userLoginLog';
const loading = ref(true);
const dataList = ref([]);
const pagination = reactive<PaginationProps>({
total: 0,
pageSize: 10,
const userLoginLogStore = useUserLoginLogStore();
const userLoginLogs = reactive({
loading: false,
datalist: [],
currentPage: 1,
pageSize: 150,
total: 100,
background: true,
layout: 'prev, pager, next',
});
async function onSearch() {
// loading.value = true;
// const { data } = await getMineLogs();
// dataList.value = data.list;
// pagination.total = data.total;
// pagination.pageSize = data.pageSize;
// pagination.currentPage = data.currentPage;
//
// setTimeout(() => {
// loading.value = false;
// }, 200);
}
/** 获取用户登录日志内容 */
const onSearchByLoginLog = async () => {
userLoginLogs.loading = true;
const data = await userLoginLogStore.getUserLoginLogListByLocalUser(userLoginLogs);
userLoginLogs.datalist = data.list;
userLoginLogs.loading = false;
};
onMounted(() => {
onSearch();
onSearchByLoginLog();
});
</script>
<template>
<div :class="['min-w-[180px]', deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]']">
<div :class="['min-w-[280px]', deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]']">
<h3 class="my-8">{{ $t('security_log') }}</h3>
<pure-table :columns="columns" :data="dataList" :loading="loading" :pagination="pagination" row-key="id" table-layout="auto" />
<pure-table :columns="columns" :data="userLoginLogs.datalist" :loading="userLoginLogs.loading" :pagination="userLoginLogs" row-key="id" table-layout="auto" />
</div>
</template>

View File

@ -1,41 +1,77 @@
import dayjs from 'dayjs';
import { reactive } from 'vue';
import type { FormRules } from 'element-plus';
import { $t } from '@/plugins/i18n';
import ProfileIcon from '@iconify-icons/ri/user-3-line';
import Profile from '@/views/account-settings/profile.vue';
import SecurityLogIcon from '@iconify-icons/ri/window-line';
import SecurityLog from '@/views/account-settings/security-log.vue';
import AccountManagementIcon from '@iconify-icons/ri/profile-line';
import AccountManagement from '@/views/account-settings/account-management.vue';
export const columns: TableColumnList = [
// { type: 'index', index: (index: number) => index + 1, label: '序号', width: 60 },
// // 用户名
// { label: $t('userLoginLog_username'), prop: 'username', width: 180 },
// // 登录Ip
// { label: $t('userLoginLog_ipAddress'), prop: 'ipAddress', width: 140 },
// 登录Ip归属地
{ label: $t('userLoginLog_ipRegion'), prop: 'ipRegion' },
// // 登录时代理
// { label: $t('userLoginLog_userAgent'), prop: 'userAgent', width: 200 },
// 操作类型
{ label: $t('userLoginLog_type'), prop: 'type' },
// // 标识客户端是否是通过Ajax发送请求的
// { label: $t('userLoginLog_xRequestedWith'), prop: 'xRequestedWith', width: 150 },
// 用户代理的品牌和版本
{
label: '详情',
prop: 'summary',
minWidth: 140,
label: $t('userLoginLog_secChUa'),
prop: 'secChUa',
formatter: ({ secChUa }) => {
const regex = /"([^"]+)";/;
const match = regex.exec(secChUa);
return match ? match[1] : secChUa;
},
},
// // 用户代理是否在手机设备上运行
// { label: $t('userLoginLog_secChUaMobile'), prop: 'secChUaMobile', width: 130 },
// 用户代理的底层操作系统/平台
{ label: $t('userLoginLog_secChUaPlatform'), prop: 'secChUaPlatform' },
// 创建时间也就是操作时间
{
label: 'IP 地址',
prop: 'ip',
minWidth: 100,
},
{
label: '地点',
prop: 'address',
minWidth: 140,
},
{
label: '操作系统',
prop: 'system',
minWidth: 100,
},
{
label: '浏览器类型',
prop: 'browser',
minWidth: 100,
},
{
label: '时间',
prop: 'operatingTime',
minWidth: 180,
label: $t('op_time'),
prop: 'op_time',
sortable: true,
width: 160,
formatter: ({ operatingTime }) => dayjs(operatingTime).format('YYYY-MM-DD HH:mm:ss'),
},
];
// 修改用户信息规则校验
export const rules = reactive<FormRules<any>>({
nickname: [{ required: true, message: '昵称必填', trigger: 'blur' }],
username: [{ required: true, message: '昵称必填', trigger: 'blur' }],
nickName: [{ required: true, message: '昵称必填', trigger: 'blur' }],
email: [{ required: true, message: '昵称必填', trigger: 'blur' }],
});
// tab栏内容
export const panes = [
{
key: 'profile',
label: '个人信息',
icon: ProfileIcon,
component: Profile,
},
{
key: 'securityLog',
label: '安全日志',
icon: SecurityLogIcon,
component: SecurityLog,
},
{
key: 'accountManagement',
label: '账户管理',
icon: AccountManagementIcon,
component: AccountManagement,
},
];

View File

@ -1,18 +1,53 @@
import { useUserStore } from '@/store/system/user';
import { reactive } from 'vue';
import { reactive, ref } from 'vue';
import { createFormData } from '@pureadmin/utils';
import { fetchUploadFile } from '@/api/v1/system';
import { message } from '@/utils/message';
import { $t } from '@/plugins/i18n';
// 剪裁完成后头像数据
export const cropperBlob = ref();
// 上传地址路径
export const uploadAvatarSrc = ref();
// 剪裁头像是否显示
export const isShow = ref(false);
const userStore = useUserStore();
// 用户信息内容
export const userInfos = reactive({
avatar: '',
username: '',
nickname: '',
email: '',
phone: '',
description: '',
summary: '',
nickName: '',
password: '',
sex: '',
});
export const onSearch = async () => {
/** 获取用户信息内容 */
export const onSearchByUserinfo = async () => {
const data = await userStore.getUserinfo();
Object.assign(userInfos, data);
userInfos.description = data.personDescription;
};
/** 修改头像 */
export const handleSubmitImage = async () => {
// 上传头像表单
const formData = createFormData({
files: new File([cropperBlob.value], 'avatar'),
type: 'avatar',
});
// 上传头像
const result = await fetchUploadFile(formData);
// 上传成功后关闭弹窗
if (result.code === 200) {
uploadAvatarSrc.value = result.data.filepath;
userInfos.avatar = result.data.url;
message($t('upload_success'), { type: 'success' });
isShow.value = false;
}
};