feat: 个人信息页面.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 登陆请求
|
||||
* 登录请求
|
||||
*/
|
||||
export interface LoginRequest {
|
||||
username?: string;
|
||||
@@ -9,7 +9,7 @@ export interface LoginRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 登陆响应
|
||||
* 登录响应
|
||||
*/
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
@@ -24,7 +24,7 @@ export interface UserUpdatePasswordRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 登陆
|
||||
* 登录
|
||||
*/
|
||||
export function login(data: LoginRequest) {
|
||||
return axios.post<LoginResponse>('/infra/auth/login', data);
|
||||
|
||||
75
orion-ops-ui/src/api/user/operator-log.ts
Normal file
75
orion-ops-ui/src/api/user/operator-log.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { DataGrid, Pagination } from '@/types/global';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 操作日志查询参数
|
||||
*/
|
||||
export interface OperatorLogQueryRequest extends Pagination {
|
||||
userId?: number;
|
||||
username?: string;
|
||||
module?: string;
|
||||
type?: string;
|
||||
riskLevel?: string;
|
||||
result?: number;
|
||||
startTimeStart?: string;
|
||||
startTimeEnd?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作日志查询响应
|
||||
*/
|
||||
export interface OperatorLogQueryResponse {
|
||||
id: number;
|
||||
userId: number;
|
||||
username: string;
|
||||
traceId: string;
|
||||
address: string;
|
||||
location: string;
|
||||
userAgent: string;
|
||||
riskLevel: string;
|
||||
module: string;
|
||||
type: string;
|
||||
logInfo: string;
|
||||
extra: string;
|
||||
result: number;
|
||||
errorMessage: string;
|
||||
returnValue: string;
|
||||
duration: number;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
createTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录日志查询响应
|
||||
*/
|
||||
export interface LoginHistoryQueryResponse {
|
||||
id: number;
|
||||
address: string;
|
||||
location: string;
|
||||
userAgent: string;
|
||||
result: number;
|
||||
errorMessage: string;
|
||||
createTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页操作日志
|
||||
*/
|
||||
export function getOperatorLogPage(request: OperatorLogQueryRequest) {
|
||||
return axios.post<DataGrid<OperatorLogQueryResponse>>('/infra/operator-log/query', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询登录日志
|
||||
*/
|
||||
export function getLoginHistory(username: string) {
|
||||
return axios.get<LoginHistoryQueryResponse[]>('/infra/operator-log/login-history', { params: { username } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前用户登录日志
|
||||
*/
|
||||
export function getCurrentLoginHistory() {
|
||||
return axios.get<LoginHistoryQueryResponse[]>('/infra/operator-log/current-login-history');
|
||||
}
|
||||
@@ -12,8 +12,6 @@ export interface UserCreateRequest {
|
||||
avatar?: string;
|
||||
mobile?: string;
|
||||
email?: string;
|
||||
status?: number;
|
||||
lastLoginTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,6 +91,20 @@ export function resetUserPassword(request: UserUpdateRequest) {
|
||||
return axios.put('/infra/system-user/reset-password', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前用户
|
||||
*/
|
||||
export function getCurrentUser() {
|
||||
return axios.get<UserQueryResponse>('/infra/system-user/get-current');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前用户
|
||||
*/
|
||||
export function updateCurrentUser(request: UserUpdateRequest) {
|
||||
return axios.put('/infra/system-user/update-current', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 id 查询用户
|
||||
*/
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
<template #content>
|
||||
<!-- 个人中心 -->
|
||||
<a-doption>
|
||||
<a-space @click="$router.push({ name: 'userMine' })">
|
||||
<a-space @click="$router.push({ name: 'userInfo' })">
|
||||
<icon-user />
|
||||
<span>个人中心</span>
|
||||
</a-space>
|
||||
|
||||
@@ -17,9 +17,9 @@ const USER: AppRouteRecordRaw = {
|
||||
component: () => import('@/views/user/user/index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'userMine',
|
||||
path: '/user/mine',
|
||||
component: () => import('@/views/user/mine/index.vue'),
|
||||
name: 'userInfo',
|
||||
path: '/user/info',
|
||||
component: () => import('@/views/user/info/index.vue'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -56,9 +56,9 @@ export default defineStore('user', {
|
||||
username: loginForm.username,
|
||||
password: md5(loginForm.password as string),
|
||||
};
|
||||
// 执行登陆
|
||||
// 执行登录
|
||||
const res = await userLogin(loginRequest);
|
||||
// 设置登陆 token
|
||||
// 设置登录 token
|
||||
setToken(res.data.token);
|
||||
} catch (err) {
|
||||
clearToken();
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
if (!errors) {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 执行登陆
|
||||
// 执行登录
|
||||
await userStore.login(values);
|
||||
// 跳转路由
|
||||
const { redirect, ...othersQuery } = router.currentRoute.value.query;
|
||||
|
||||
@@ -76,6 +76,10 @@
|
||||
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size) => fetchTableData(1, size)"
|
||||
:bordered="false">
|
||||
<!-- 配置项 -->
|
||||
<template #keyName="{record}">
|
||||
{{ record.keyName }}<span style="margin: 0 4px;">-</span>{{ record.keyDescription }}
|
||||
</template>
|
||||
<!-- 值 -->
|
||||
<template #value="{ record }">
|
||||
<span class="copy-left" title="复制" @click="copy(record.value)">
|
||||
|
||||
@@ -15,10 +15,7 @@ const columns = [
|
||||
slotName: 'keyName',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
render: ({ record }) => {
|
||||
return `${record.keyName} - ${record.keyDescription}`;
|
||||
},
|
||||
tooltip: true
|
||||
}, {
|
||||
title: '配置描述',
|
||||
dataIndex: 'label',
|
||||
|
||||
131
orion-ops-ui/src/views/user/info/components/login-history.vue
Normal file
131
orion-ops-ui/src/views/user/info/components/login-history.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" class="main-container">
|
||||
<span class="extra-message">只展示最近登录的 30 条历史记录</span>
|
||||
<a-timeline>
|
||||
<a-timeline-item v-for="item in list"
|
||||
:key="item.id">
|
||||
<!-- 图标 -->
|
||||
<template #dot>
|
||||
<div class="icon-container">
|
||||
<icon-desktop />
|
||||
</div>
|
||||
</template>
|
||||
<!-- 日志行 -->
|
||||
<div class="log-line">
|
||||
<!-- 地址行 -->
|
||||
<span class="address-line">
|
||||
<span class="mr8">{{ item.address }}</span>
|
||||
<span>{{ item.location }}</span>
|
||||
</span>
|
||||
<!-- 错误信息行 -->
|
||||
<span class="error-line" v-if="item.result === ResultStatus.FAILED">
|
||||
登录失败: {{ item.errorMessage }}
|
||||
</span>
|
||||
<!-- 时间行 -->
|
||||
<span class="time-line">
|
||||
{{ dateFormat(new Date(item.createTime)) }}
|
||||
</span>
|
||||
<!-- ua -->
|
||||
<span class="ua-line">
|
||||
{{ item.userAgent }}
|
||||
</span>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'login-history'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { LoginHistoryQueryResponse } from '@/api/user/operator-log';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useUserStore } from '@/store';
|
||||
import { ResultStatus } from '../types/const';
|
||||
import { getCurrentLoginHistory } from '@/api/user/operator-log';
|
||||
import { dateFormat } from '@/utils';
|
||||
|
||||
const list = ref<LoginHistoryQueryResponse[]>([]);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
// 查询操作日志
|
||||
onMounted(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getCurrentLoginHistory();
|
||||
list.value = data;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.main-container {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
padding-left: 48px;
|
||||
}
|
||||
|
||||
.extra-message {
|
||||
margin-bottom: 38px;
|
||||
margin-left: -20px;
|
||||
display: block;
|
||||
color: var(--color-text-3);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
border-radius: 50%;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: var(--color-fill-4);
|
||||
font-size: 28px;
|
||||
color: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.arco-timeline-item-content-wrapper) {
|
||||
position: relative;
|
||||
margin-left: 44px;
|
||||
margin-top: -22px;
|
||||
}
|
||||
|
||||
:deep(.arco-timeline-item) {
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.address-line {
|
||||
color: var(--color-text-1);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.time-line, .ua-line, .error-line {
|
||||
color: var(--color-text-3);
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.error-line {
|
||||
color: rgb(var(--danger-6));
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
112
orion-ops-ui/src/views/user/info/components/user-info.vue
Normal file
112
orion-ops-ui/src/views/user/info/components/user-info.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 400px;">
|
||||
<!-- 头像 -->
|
||||
<div class="avatar-container">
|
||||
<div class="avatar-wrapper">
|
||||
<a-avatar :size="88"
|
||||
:style="{ backgroundColor: '#3370ff' }">
|
||||
{{ nickname }}
|
||||
</a-avatar>
|
||||
</div>
|
||||
</div>
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
size="medium"
|
||||
:style="{ width: '100%' }"
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 18 }"
|
||||
:rules="formRules">
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="username" label="用户名">
|
||||
<a-input v-model="formModel.username" disabled />
|
||||
</a-form-item>
|
||||
<!-- 花名 -->
|
||||
<a-form-item field="nickname" label="花名">
|
||||
<a-input v-model="formModel.nickname" placeholder="请输入花名" />
|
||||
</a-form-item>
|
||||
<!-- 手机号 -->
|
||||
<a-form-item field="mobile" label="手机号">
|
||||
<a-input v-model="formModel.mobile" placeholder="请输入手机号" />
|
||||
</a-form-item>
|
||||
<!-- 邮箱 -->
|
||||
<a-form-item field="email" label="邮箱">
|
||||
<a-input v-model="formModel.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<!-- 操作 -->
|
||||
<div class="handler-container">
|
||||
<a-button type="primary" @click="save">保存</a-button>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'user-info'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UserUpdateRequest } from '@/api/user/user';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import formRules from '../../user/types/form.rules';
|
||||
import { useUserStore } from '@/store';
|
||||
import { getCurrentUser, updateCurrentUser } from '@/api/user/user';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const formRef = ref();
|
||||
const formModel = ref<UserUpdateRequest>({});
|
||||
|
||||
// 用户名
|
||||
const nickname = computed(() => userStore.nickname?.substring(0, 1));
|
||||
|
||||
// 保存
|
||||
const save = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateCurrentUser(formModel.value);
|
||||
userStore.nickname = formModel.value.nickname;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载用户信息
|
||||
onMounted(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await getCurrentUser();
|
||||
formModel.value = pick(data, 'id', 'username', 'nickname', 'mobile', 'email');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 4px 0;
|
||||
|
||||
.avatar-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
width: calc(100% / 24 * 18);
|
||||
}
|
||||
}
|
||||
|
||||
.handler-container {
|
||||
display: flex;
|
||||
margin-left: calc(100% / 24 * 6);
|
||||
}
|
||||
</style>
|
||||
55
orion-ops-ui/src/views/user/info/index.vue
Normal file
55
orion-ops-ui/src/views/user/info/index.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="tabs-container">
|
||||
<a-tabs type="rounded"
|
||||
size="medium"
|
||||
position="left"
|
||||
:lazy-load="true"
|
||||
:destroy-on-hide="true">
|
||||
<!-- 个人信息 -->
|
||||
<a-tab-pane key="1" title="个人信息">
|
||||
<user-info />
|
||||
</a-tab-pane>
|
||||
<!-- 登录日志 -->
|
||||
<a-tab-pane key="2" title="登录日志">
|
||||
<login-history />
|
||||
</a-tab-pane>
|
||||
<!-- 登录设备 -->
|
||||
<a-tab-pane key="3" title="登录设备">
|
||||
<login-history />
|
||||
</a-tab-pane>
|
||||
<!-- 操作日志 -->
|
||||
<a-tab-pane key="4" title="操作日志">
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'userInfo'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UserInfo from './components/user-info.vue';
|
||||
import LoginHistory from './components/login-history.vue';
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tabs-container {
|
||||
background: #FFFFFF;
|
||||
margin: 16px 16px 0 16px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-pane) {
|
||||
border-left: 1px var(--color-neutral-3) solid;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-tab-title) {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
7
orion-ops-ui/src/views/user/info/types/const.ts
Normal file
7
orion-ops-ui/src/views/user/info/types/const.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// 结果状态
|
||||
export const ResultStatus = {
|
||||
// 失败
|
||||
FAILED: 0,
|
||||
// 成功
|
||||
SUCCESS: 1,
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<a-row class="layout-container">
|
||||
<a-col :span="8">
|
||||
<a-card class="general-card">
|
||||
<template #title>
|
||||
用户信息
|
||||
</template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 16 }"
|
||||
:rules="formRules">
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="username" label="用户名">
|
||||
<a-input v-model="formModel.username" disabled />
|
||||
</a-form-item>
|
||||
<!-- 花名 -->
|
||||
<a-form-item field="nickname" label="花名">
|
||||
<a-input v-model="formModel.nickname" placeholder="请输入花名" />
|
||||
</a-form-item>
|
||||
<!-- 手机号 -->
|
||||
<a-form-item field="mobile" label="手机号">
|
||||
<a-input v-model="formModel.mobile" placeholder="请输入手机号" />
|
||||
</a-form-item>
|
||||
<!-- 邮箱 -->
|
||||
<a-form-item field="email" label="邮箱">
|
||||
<a-input v-model="formModel.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UserUpdateRequest } from '@/api/user/user';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { ref } from 'vue';
|
||||
import formRules from '../user/types/form.rules';
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const formRef = ref();
|
||||
const formModel = ref<UserUpdateRequest>({});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user